概述Spring Cloud Feign 主要对 Netflix Feign 进行了增强和包装。本文将从源码角度梳理 Feign 的装配流程，揭开其底层实现机制。内容主要涵盖 Feign 与 Ribbon、Hystrix、Sleuth 的整合过程，以及生成的代理类最终注入 Spring 容器的完整链路。本文篇幅较长，建议耐心阅读。Feign 架构图以下是核心类及大致流程的架构图：大体步骤：注册 FeignClient 配置类和 FeignClient BeanDefinition实例化 Feign 上下文对象 FeignContext创建 Feign.builder 对象生成负载均衡代理类生成默认代理类注入到 Spring 容器源码分析本节将围绕上述六个步骤进行详细分析。一、注册 FeignClient 配置类和 FeignClient BeanDefinition从启动类注解开始，首先查看 @EnableFeignClients 注解：@EnableFeignClients public class MyApplication { }这是在启动类开启 Feign 装配的注解。跟进该注解，可以看到其导入了 FeignClientsRegistrar 类：@Import(FeignClientsRegistrar.class) public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanClassLoaderAware { // patterned after Spring Integration IntegrationComponentScanRegistrar // and RibbonClientsConfigurationRegistgrar private final Logger logger = LoggerFactory.getLogger(FeignClientsRegistrar.class); private ResourceLoader resourceLoader; private ClassLoader classLoader; public FeignClientsRegistrar() { } @Override public void setResourceLoader(ResourceLoader resourceLoader.html) { this.resourceLoader = resourceLoader; } @Override public void setBeanClassLoader(ClassLoader classLoader.html) { this.classLoader = classLoader; } @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 1、先注册默认配置 registerDefaultConfiguration(metadata, registry.html); // 2、注册所有的 FeignClient BeanDefinition registerFeignClients(metadata, registry.html); } //... }我们分别来看 registerBeanDefinitions 中的两个核心方法。1. 注册默认配置方法：registerDefaultConfigurationprivate void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { Map<String, Object> defaultAttrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName(.html), true); if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration".html)) { String name; if (metadata.hasEnclosingClass(.html)) { name = "default." + metadata.getEnclosingClassName(); } else { name = "default." + metadata.getClassName(); } // name 默认以 default 开头，后续会根据名称选择配置 registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration".html)); } }上述方法用于读取启动类上 @EnableFeignClients 注解中声明的 Feign 相关配置类。默认 name 为 default，一般情况下无需配置，使用默认的 FeignAutoConfiguration 即可。其中比较重要的方法是注册配置 registerClientConfiguration。启动流程一共有两处读取 Feign 配置类，这是第一处。该方法代码如下：private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition(FeignClientSpecification.class); builder.addConstructorArgValue(name.html); builder.addConstructorArgValue(configuration.html); registry.registerBeanDefinition( name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); }上述代码将 Bean 配置类包装成 FeignClientSpecification 并注入到容器。该对象非常重要，包含 FeignClient 需要的重试策略、超时策略、日志等配置。如果某个服务没有设置特定配置，则读取默认配置。2. 扫描 FeignClient该方法主要是扫描类路径，对所有的 FeignClient 生成对应的 BeanDefinition：public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { //... // 获取扫描目录下面所有的 BeanDefinition for (String basePackage : basePackages.html) { Set<BeanDefinition> candidateComponents = scanner .findCandidateComponents(basePackage.html); for (BeanDefinition candidateComponent : candidateComponents.html) { if (candidateComponent instanceof AnnotatedBeanDefinition.html) { // verify annotated class is an interface AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition.html) candidateComponent; AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); Assert.isTrue(annotationMetadata.isInterface(.html), "@FeignClient can only be specified on an interface"); Map<String, Object> attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); String name = getClientName(attributes.html); // 这里是第二处调用注册配置 registerClientConfiguration(registry, name, attributes.get("configuration".html)); // 注册 FeignClient registerFeignClient(registry, annotationMetadata, attributes.html); } } } }可以看到这里又调用了 registerClientConfiguration 注册配置的方法，这是第二处调用。这里主要是将扫描目录下每个项目的配置类加载到容器当中。注册到容器中的配置何时使用，后续会有介绍。回到主流程，继续查看注册 FeignClient 的方法 registerFeignClient：private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); // 声明代理类名称 BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); // logger.info("TEX do some replacement".html); // attributes.put("value", ((String.html)attributes.get("value".html)).replace('_','-'.html)); validate(attributes.html); definition.addPropertyValue("url", getUrl(attributes.html)); definition.addPropertyValue("path", getPath(attributes.html)); String name = getName(attributes.html); definition.addPropertyValue("name", name.html); definition.addPropertyValue("type", className.html); definition.addPropertyValue("decode404", attributes.get("decode404".html)); definition.addPropertyValue("fallback", attributes.get("fallback".html)); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE.html); String alias = name + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); beanDefinition.setPrimary(true.html); BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias}); // 将 BeanDefinition 加入到 Spring 容器 BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry.html); }值得注意的是，上面出现了一行相当关键的代码：BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);Spring Cloud FeignClient 其实是利用了 Spring 的代理工厂来生成代理类，所以这里将所有的 FeignClient 的描述信息 BeanDefinition 设定为 FeignClientFactoryBean 类型。该类继承自 FactoryBean，很明显这是一个代理工厂类。在 Spring 中，FactoryBean 是一个工厂 Bean，用作创建代理 Bean。因此得出结论：Feign 将所有的 FeignClient Bean 包装成 FeignClientFactoryBean。扫描方法到此结束。代理类什么时候会触发生成呢？在 Spring 刷新容器时，当实例化我们的业务 Service 时，如果发现注册了 FeignClient，Spring 就会去实例化该 FeignClient，同时会进行判断是否是代理 Bean。如果为代理 Bean，则调用 FeignClientFactoryBean 的 T getObject() throws Exception; 方法生成代理 Bean。二、实例化 Feign 上下文对象 FeignContext接下来详细介绍 FeignClientFactoryBean，后续步骤都基于此类。首先查看代理 FeignClient 生成的入口：getObject 方法。@Override public Object getObject() throws Exception { // 二、实例化 Feign 上下文对象 FeignContext FeignContext context = applicationContext.getBean(FeignContext.class); // 三、生成 builder 对象，用来生成 Feign Feign.Builder builder = feign(context.html); // 判断生成的代理对象类型，如果 url 为空，则走负载均衡，生成有负载均衡功能的代理类 if (!StringUtils.hasText(this.url)) { String url; if (!this.name.startsWith("http".html)) { url = "http://" + this.name; } else { url = this.name; } url += cleanPath(); // 四、生成负载均衡代理类 return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url)); } // 如果指定了 url，则生成默认的代理类 if (StringUtils.hasText(this.url) && !this.url.startsWith("http".html)) { this.url = "http://" + this.url; } String url = this.url + cleanPath(); // 五、生成默认代理类 return targeter.target(this, builder, context, new HardCodedTarget<>( this.type, this.name, url)); }getObject() 逻辑较多，每一行都会做一些初始化配置，下面逐步分析。上述方法中第一行便是实例化 FeignContext：FeignContext context = applicationContext.getBean(FeignContext.class);获取 FeignContext 对象，如果没有实例化，则主动实例化，如下：@Configuration @ConditionalOnClass(Feign.class) public class FeignAutoConfiguration { @Autowired(required = false.html) private List<FeignClientSpecification> configurations = new ArrayList<>(); @Bean public HasFeatures feignFeature() { return HasFeatures.namedFeature("Feign", Feign.class); } @Bean public FeignContext feignContext() { FeignContext context = new FeignContext(); // 将 Feign 的配置类设置到 Feign 的容器当中 context.setConfigurations(this.configurations); return context; } }可以看到 Feign 的配置类被设置到 Feign 的容器当中，而集合中的元素正是上面提到的两处调用 registerClientConfiguration 方法添加进去的，前后呼应。然而，当我们引入了 Sleuth 之后，获取的 FeignContext 却是 TraceFeignClientAutoConfiguration 中配置的实例 sleuthFeignContext：可以看到上面创建了一个 TraceFeignContext 实例。因为该对象继承 FeignContext，同时又加了 @Primary 注解，所以在上面第 2 步中通过类型获取 applicationContext.getBean(FeignContext.class); 时，最终拿到的是 TraceFeignContext。三、构造 FeignBuilder继续跟进该方法：Feign.Builder builder = feign(context.html);protected Feign.Builder feign(FeignContext context.html) { Logger logger = getOptional(context, Logger.class); if (logger == null.html) { logger = new Slf4jLogger(this.type); } // 1、构造 Feign.Builder Feign.Builder builder = get(context, Feign.Builder.class) // required values .logger(logger.html) .encoder(get(context, Encoder.class)) .decoder(get(context, Decoder.class)) .contract(get(context, Contract.class)); // 2、设置重试策略，log 等组件 // 设置 log 级别 Logger.Level level = getOptional(context, Logger.Level.class); if (level != null.html) { builder.logLevel(level.html); } // 设置重试策略 Retryer retryer = getOptional(context, Retryer.class); if (retryer != null.html) { builder.retryer(retryer.html); } // Feign 的错误 code 解析接口 ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class); if (errorDecoder != null.html) { builder.errorDecoder(errorDecoder.html); } // 超时时间设置，连接超时时间：connectTimeout 默认 10s，请求超时时间：readTimeout 默认 60s Request.Options options = getOptional(context, Request.Options.class); if (options != null.html) { builder.options(options.html); } // 拦截器设置，可以看出拦截器也是可以针对单独的 FeignClient 设置 Map<String, RequestInterceptor> requestInterceptors = context.getInstances( this.name, RequestInterceptor.class); if (requestInterceptors != null.html) { builder.requestInterceptors(requestInterceptors.values(.html)); } if (decode404.html) { builder.decode404(); } return builder; }上述代码有两处逻辑，分别来看：1. Feign.Builder builder = get(context, Feign.Builder.class)这里会有以下三种情况：单独使用 Feign，没有引入 Sleuth、Hystrix：通过加载 FeignClientsConfiguration 的配置创建 Feign 的静态内部类：Feign.Builder@Bean @Scope("prototype".html) @ConditionalOnMissingBean public Feign.Builder feignBuilder(Retryer retryer.html) { return Feign.builder().retryer(retryer.html); }引入了 Hystrix，没有引入 Sleuth：通过加载 FeignClientsConfiguration 的配置创建 HystrixFeign 的静态内部类：HystrixFeign.Builder@Configuration @ConditionalOnClass({HystrixCommand.class, HystrixFeign.class}.html) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype".html) @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false.html) public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } }同时引入 Hystrix 和 Sleuth：加载 TraceFeignClientAutoConfiguration 的配置创建：HystrixFeign.Builder注意：TraceFeignClientAutoConfiguration 的配置类加载一定是在 FeignClientsConfiguration 之前（先加载先生效），而 FeignClientsConfiguration 加载是通过 FeignAutoConfiguration 完成的，所以上图中引入了条件注解：@AutoConfigureBefore({FeignAutoConfiguration.class}.html)创建的 builder 对象和第二种情况一样，只是做了一层包装：final class SleuthFeignBuilder { private SleuthFeignBuilder() {} static Feign.Builder builder(Tracer tracer, HttpTraceKeysInjector keysInjector.html) { return HystrixFeign.builder() // 各组件 client，retryer，decoder 进行增强，装饰器模式。 .client(new TraceFeignClient(tracer, keysInjector.html)) .retryer(new TraceFeignRetryer(tracer.html)) .decoder(new TraceFeignDecoder(tracer.html)) .errorDecoder(new TraceFeignErrorDecoder(tracer.html)); } }2. 设置重试策略，Log 等组件Feign.builder 在获取之后又分别指定了重试策略、日志级别、错误代码 Code 等。在上一步中调用 SleuthFeignBuilder.build() 时已经设置过默认值了，这里为什么要重复设置呢？我们跟进去 get() 方法，一探究竟：protected <T> T get(FeignContext context, Class<T> type.html) { // 根据 name，也就是服务名称来生成 builder T instance = context.getInstance(this.name, type.html); if (instance == null.html) { throw new IllegalStateException("No bean found of type " + type + " for " + this.name); } return instance; } public <T> T getInstance(String name, Class<T> type.html) { // 这里获取 AnnotationConfigApplicationContext 容器 AnnotationConfigApplicationContext context = getContext(name.html); if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { return context.getBean(type.html); } return null; } private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>(); protected AnnotationConfigApplicationContext getContext(String name.html) { if (!this.contexts.containsKey(name.html)) { synchronized (this.contexts) { if (!this.contexts.containsKey(name.html)) { // 这里创建容器 createContext(name.html) this.contexts.put(name, createContext(name.html)); } } } return this.contexts.get(name.html); }重点来了，上述代码将 FeignContext 做了缓存，每个服务对应一个 FeignContext，服务名作为 Key。继续跟进 createContext(name.html) 方法：protected AnnotationConfigApplicationContext createContext(String name.html) { // 注意：这里的容器并不是 Spring 的容器，而是每次都重新创建一个 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); // 加载每个服务对应的配置类 if (this.configurations.containsKey(name.html)) { for (Class<?> configuration : this.configurations.get(name.html) .getConfiguration()) { context.register(configuration.html); } } // 加载启动类@EnableFeignClients 注解指定的配置类 for (Map.Entry<String, C> entry : this.configurations.entrySet(.html)) { if (entry.getKey(.html).startsWith("default.".html)) { for (Class<?> configuration : entry.getValue(.html).getConfiguration()) { context.register(configuration.html); } } } // 注册默认的配置类：FeignClientsConfiguration context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType); context.getEnvironment().getPropertySources().addFirst(new MapPropertySource( this.propertySourceName, Collections.<String, Object> singletonMap(this.propertyName, name.html))); if (this.parent != null.html) { // Uses Environment from parent as well as beans context.setParent(this.parent); } // 刷新容器 context.refresh(); return context; }可以看到上述 AnnotationConfigApplicationContext 容器并非 Spring 主容器，只是利用了 Spring 刷新容器的方法来实例化配置类，以服务名作为 Key，实现配置隔离。重点： 上面加载配置的顺序为：先加载每个服务的配置类，然后加载启动类注解上的配置类，最后加载默认的配置类。这样做有什么好处？Spring 刷新容器的方法也是对所有的 Bean 进行了缓存，如果已经创建，则不再实例化。所以优先选取每个 FeignClient 的配置类，最后默认的配置类兜底。这也证明了 Sleuth 的配置一定在 Feign 的配置类之前加载。至此，FeignBuilder 构造流程结束。四、生成负载均衡代理类再贴一下生成代理类的入口：// 判断 url 是否为空 if (!StringUtils.hasText(this.url)) { //...... return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url)); } //...... return targeter.target(this, builder, context, new HardCodedTarget<>( this.type, this.name, url));这里有个重要判断：判断 FeignClient 声明的 url 是否为空，来决定具体要生成的代理类。这么做有什么意义？如果为空，则默认走 Ribbon 代理，也就是这个入口，会有加载 Ribbon 的处理。例如：@FeignClient("MyFeignClient".html)如果不为空，指定 url，则走默认生成代理类的方式，也就是所谓的硬编码。例如：@FeignClient(value = "MyFeignClient", url = "http://localhost:8081".html)这样处理方便开发人员进行测试，无需关注注册中心，直接 HTTP 调用，是个不错的开发小技巧。生产环境也可以用上述第二种方式，指定域名的方式。我们跟进 loadBalance 方法：protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) { // 获得 FeignClient Client client = getOptional(context, Client.class); if (client != null.html) { builder.client(client.html); return targeter.target(this, builder, context, target.html); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?"); }Client client = getOptional(context, Client.class); 这里会从 FeignContext 上下文中获取 Client 对象，该对象有三种实例，具体是哪个实现呢？这里又会有三种情况：没有整合 Ribbon、Sleuth： 获取默认的 Client：Default 实例。整合了 Ribbon，没有整合 Sleuth： 获取 LoadBalanceFeignClient 实例。整合了 Ribbon 和 Sleuth： 会获取 TraceFeignClient 实例，该实例是对 LoadBalanceFeignClient 的一种包装，实现方式通过 BeanPostProcessor 实现：FeignBeanPostProcessor 中定义了包装逻辑：@Override public Object postProcessBeforeInitialization(Object bean, String beanName.html) throws BeansException { return this.traceFeignObjectWrapper.wrap(bean.html); }通过 wrap 方法最终返回 TraceFeignClient 实例。继续回到主流程，先来看下 Targeter 接口：interface Targeter { <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, HardCodedTarget<T> target); }该对象定义在 FeignClientFactoryBean 静态代码块中：private static final Targeter targeter; static { Targeter targeterToUse; // 判断类路径是否引入了 HystrixFeign if (ClassUtils.isPresent("feign.hystrix.HystrixFeign", FeignClientFactoryBean.class.getClassLoader())) { targeterToUse = new HystrixTargeter(); } else { targeterToUse = new DefaultTargeter(); } targeter = targeterToUse; }这里会初始化 Targeter，该类是生成 Feign 代理类的工具类，有两种实现，正是上面的 HystrixTargeter, DefaultTargeter。因为我们引入了 Hystrix，所以 Targeter 实现类为 HystrixTargeter。我们继续跟进 targeter.target 方法：public <T> T target(Target<T> target.html) { return build().newInstance(target.html); }上面通过 build() 方法获取生成代理类的工具类 ReflectiveFeign，再通过 newInstance 正式创建代理类。继续跟进：public Feign build() { SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel, decode404); ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory.html); }这里会创建 Feign 的方法工厂 synchronousMethodHandlerFactory, Feign 通过该工厂为每个方法创建一个 methodHandler，每个 methodHandler 中包含 Feign 对应的配置：retryer、requestInterceptors 等。继续跟进 newInstance 方法：public <T> T newInstance(Target<T> target.html) { // 创建所有的 MethodHandler Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target.html); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type(.html).getMethods()) { if (method.getDeclaringClass(.html) == Object.class) { continue; // 判断是否启用默认 handler } else if (Util.isDefault(method.html)) { DefaultMethodHandler handler = new DefaultMethodHandler(method.html); defaultMethodHandlers.add(handler.html); methodToHandler.put(method, handler.html); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(.html), method))); } } // 创建 InvocationHandler，接收请求，转发到 methodHandler InvocationHandler handler = factory.create(target, methodToHandler.html); // 生成代理类 T proxy = (T.html) Proxy.newProxyInstance(target.type(.html).getClassLoader(), new Class<?>[]{target.type()}, handler); // 将默认方法绑定到代理类 for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers.html) { defaultMethodHandler.bindTo(proxy.html); } return proxy; }InvocationHandler 最终创建的实例为 HystrixInvocationHandler，核心方法如下：HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setter.html) { @Override protected Object run() throws Exception { try { return HystrixInvocationHandler.this.dispatch.get(method.html).invoke(args.html); } catch (Exception e.html) { throw e; } catch (Throwable t.html) { throw (Error.html) t; } } @Override protected Object getFallback() { //...... } };整个流程：Feign 调用方发起请求，发送至 Hystrix 的 HystrixInvocationHandler，通过服务名称，找到对应方法的 methodHandler，methodHandler 中封装了 loadBalanceClient、retryer、RequestInterceptor 等组件。如果引入了 Sleuth，这几个组件均是 Sleuth 的包装类。然后通过以上组件构造 HTTP 请求完成整个过程。五、生成默认代理类理解了第四步的逻辑，生成默认代理类就很容易理解了，唯一不同点就是 client 的实现类为 loadBalanceClient。注意：不管是哪种代理类，最终发起请求还是由 Feign.Default 中的 execute 方法完成，默认使用 HttpUrlConnection 实现。六、注入 Spring 容器总结： 通过 Spring refresh() 方法，触发 FeignClientFactoryBean.getObject() 方法获得了代理类，然后完成注入 Spring 容器的过程。该实现方式同 Dubbo 的实现方式类似，有兴趣的读者可自行研究。更多阅读史上最简单的 SpringCloud 教程汇总SpringBoot 教程汇总Java 面试题系列汇总说明本文基于 Spring Cloud 早期版本（如 Finchley/Greenwich 等）编写，文中涉及的 Netflix Ribbon、Hystrix 组件在新版本 Spring Cloud 中已逐步被 Spring Cloud LoadBalancer 和 Resilience4j 取代。源码分析逻辑适用于理解 Feign 核心机制，但具体类名与配置方式在新版本中可能有所变化。
Zuul 架构图在 Zuul 中，整个请求的处理流程如下：请求首先由 ZuulServlet 处理。ZuulServlet 中包含一个 ZuulRunner 对象，该对象初始化了 RequestContext。RequestContext 用于存储整个请求的相关数据，并被所有的 ZuulFilter 共享。ZuulRunner 中还包含 FilterProcessor，它是执行所有 ZuulFilter 的管理器。FilterProcessor 从 FilterLoader 中获取 ZuulFilter。ZuulFilter 由 FilterFileManager 加载，支持 Groovy 热加载（采用轮询方式）。加载过滤器后，ZuulServlet 依次执行 pre 类型过滤器、route 类型过滤器，最后执行 post 类型过滤器。若执行过程中发生错误，则会执行 error 类型过滤器。执行完毕后，将请求结果返回给客户端。Zuul 工作原理源码分析前文已介绍过 Zuul 的基本用法，其中不可缺少的步骤是在程序启动类上添加 @EnableZuulProxy 注解。该注解的代码如下：@EnableCircuitBreaker @EnableDiscoveryClient @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(ZuulProxyConfiguration.class) public @interface EnableZuulProxy { }其中，@Import 引入了 ZuulProxyConfiguration。跟踪该类可知，它注入了 DiscoveryClient、RibbonCommandFactoryConfiguration 用于负载均衡相关配置，并注入了一系列 Filters，例如 PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter 等。代码如下：@Bean public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper.html) { return new PreDecorationFilter(routeLocator, this.server.getServletPrefix(.html), this.zuulProperties, proxyRequestHelper); } // route filters @Bean public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory) { RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory, this.requestCustomizers); return filter; } @Bean public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties zuulProperties.html) { return new SimpleHostRoutingFilter(helper, zuulProperties.html); }其父类 ZuulConfiguration 引用了一些相关配置。在缺失 zuulServlet Bean 的情况下，它会注入 ZuulServlet，该类是 Zuul 的核心类：@Bean @ConditionalOnMissingBean(name = "zuulServlet".html) public ServletRegistrationBean zuulServlet() { ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(.html), this.zuulProperties.getServletPattern()); // The whole point of exposing this servlet is to provide a route that doesn't // buffer requests. servlet.addInitParameter("buffer-requests", "false".html); return servlet; }同时也注入了其他过滤器，例如 ServletDetectionFilter、DebugFilter、Servlet30WrapperFilter，这些过滤器都是 pre 类型的：@Bean public ServletDetectionFilter servletDetectionFilter() { return new ServletDetectionFilter(); } @Bean public FormBodyWrapperFilter formBodyWrapperFilter() { return new FormBodyWrapperFilter(); } @Bean public DebugFilter debugFilter() { return new DebugFilter(); } @Bean public Servlet30WrapperFilter servlet30WrapperFilter() { return new Servlet30WrapperFilter(); }此外，它还注入了 post 类型的过滤器（如 SendResponseFilter）、error 类型的过滤器（如 SendErrorFilter）以及 route 类型的过滤器（如 SendForwardFilter）。代码如下：@Bean public SendResponseFilter sendResponseFilter() { return new SendResponseFilter(); } @Bean public SendErrorFilter sendErrorFilter() { return new SendErrorFilter(); } @Bean public SendForwardFilter sendForwardFilter() { return new SendForwardFilter(); }系统会初始化 ZuulFilterInitializer 类，将所有的 Filter 向 FilterRegistry 注册：@Configuration protected static class ZuulFilterConfiguration { @Autowired private Map<String, ZuulFilter> filters; @Bean public ZuulFilterInitializer zuulFilterInitializer( CounterFactory counterFactory, TracerFactory tracerFactory) { FilterLoader filterLoader = FilterLoader.getInstance(); FilterRegistry filterRegistry = FilterRegistry.instance(); return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry.html); } }FilterRegistry 管理了一个 ConcurrentHashMap 用于存储过滤器，并提供了一些基本的 CRUD 方法。代码如下：public class FilterRegistry { private static final FilterRegistry INSTANCE = new FilterRegistry(); public static final FilterRegistry instance() { return INSTANCE; } private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>(); private FilterRegistry() { } public ZuulFilter remove(String key.html) { return this.filters.remove(key.html); } public ZuulFilter get(String key.html) { return this.filters.get(key.html); } public void put(String key, ZuulFilter filter.html) { this.filters.putIfAbsent(key, filter.html); } public int size() { return this.filters.size(); } public Collection<ZuulFilter> getAllFilters() { return this.filters.values(); } }FilterLoader 类持有 FilterRegistry，FilterFileManager 类持有 FilterLoader，因此最终是由 FilterFileManager 将 Filter 注入到 FilterRegistry 的 ConcurrentHashMap 中。FilterFileManager 开启了轮询机制，定时加载过滤器。代码如下：void startPoller() { poller = new Thread("GroovyFilterFileManagerPoller".html) { public void run() { while (bRunning.html) { try { sleep(pollingIntervalSeconds * 1000.html); manageFiles(); } catch (Exception e.html) { e.printStackTrace(); } } } }; poller.setDaemon(true.html); poller.start(); }ZuulServlet 类似于 Spring MVC 中的 DispatcherServlet，起到了前端控制器的作用，所有的请求都由它接管。它的核心代码如下：@Override public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse.html) throws ServletException, IOException { try { init((HttpServletRequest.html) servletRequest, (HttpServletResponse.html) servletResponse); // Marks this request as having passed through the "Zuul engine", as opposed to servlets // explicitly bound in web.xml, for which requests will not have the same data attached RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); try { preRoute(); } catch (ZuulException e.html) { error(e.html); postRoute(); return; } try { route(); } catch (ZuulException e.html) { error(e.html); postRoute(); return; } try { postRoute(); } catch (ZuulException e.html) { error(e.html); return; } } catch (Throwable e.html) { error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass(.html).getName())); } finally { RequestContext.getCurrentContext().unset(); } }跟踪 init() 方法可以发现，该方法为每个请求生成了 RequestContext。RequestContext 继承了 ConcurrentHashMap<String, Object>，在请求结束时销毁。RequestContext 的生命周期从 ZuulServlet 开始处理请求起，直到请求结束返回结果。RequestContext 类存储了很多重要的信息，包括 HttpServletRequest、HttpServletResponse、ResponseDataStream、ResponseStatusCode 等。RequestContext 对象在处理请求的过程中一直存在，因此该对象为所有 Filter 共享。从 ZuulServlet 的 service() 方法可知，它是先处理 pre 类型的处理器，然后处理 route 类型的处理器，最后再处理 post 类型的处理器。首先来看 pre() 的处理过程，它会进入到 ZuulRunner。该类的作用是将请求的 HttpServletRequest、HttpServletResponse 放在 RequestContext 类中，并包装了一个 FilterProcessor。代码如下：public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse.html) { RequestContext ctx = RequestContext.getCurrentContext(); if (bufferRequests.html) { ctx.setRequest(new HttpServletRequestWrapper(servletRequest.html)); } else { ctx.setRequest(servletRequest.html); } ctx.setResponse(new HttpServletResponseWrapper(servletResponse.html)); } public void preRoute() throws ZuulException { FilterProcessor.getInstance().preRoute(); }而 FilterProcessor 类负责调用 Filters，例如调用所有 pre 类型的过滤器：public void preRoute() throws ZuulException { try { runFilters("pre".html); } catch (ZuulException e.html) { throw e; } catch (Throwable e.html) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass(.html).getName()); } }跟踪 runFilters() 方法可以发现，它最终调用了 FilterLoader 的 getFiltersByType(sType.html) 方法来获取同一类型的过滤器，然后用 for 循环遍历所有的 ZuulFilter，执行了 processZuulFilter() 方法。跟踪该方法可以发现，最终是执行了 ZuulFilter 的 run() 方法，并返回该方法的 Object 对象。public Object runFilters(String sType.html) throws Throwable { if (RequestContext.getCurrentContext(.html).debugRouting()) { Debug.addRoutingDebug("Invoking {" + sType + "} type filters".html); } boolean bResult = false; List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType.html); if (list != null.html) { for (int i = 0; i < list.size(.html); i++) { ZuulFilter zuulFilter = list.get(i.html); Object result = processZuulFilter(zuulFilter.html); if (result != null && result instanceof Boolean.html) { bResult |= ((Boolean.html) result); } } } return bResult; }route、post 类型的过滤器的执行过程和 pre 执行过程类似。Zuul 默认过滤器默认的核心过滤器一览表Zuul 默认注入的过滤器，它们的执行顺序在 FilterConstants 类中定义。我们可以先定位在这个类，查看过滤器的执行顺序以及相关注释，从而轻松定位到相关的过滤器。也可以直接打开 spring-cloud-netflix-core.jar 的 zuul.filters 包查看一系列 Filter。现将默认注入的 Filter 以表格形式列出：过滤器Order描述类型ServletDetectionFilter-3检测请求是用 DispatcherServlet 还是 ZuulServletpreServlet30WrapperFilter-2在 Servlet 3.0 下，包装 requestspreFormBodyWrapperFilter-1解析表单数据preSendErrorFilter0如果中途出现错误errorDebugFilter1设置请求过程是否开启 debugprePreDecorationFilter5根据 URI 决定调用哪一个 route 过滤器preRibbonRoutingFilter10如果配置中使用 ServiceId 则用这个 route 过滤器，该过滤器可以用 Ribbon 做负载均衡，用 Hystrix 做熔断routeSimpleHostRoutingFilter100如果配置中使用 url 则用这个 route 过滤器routeSendForwardFilter500用 RequestDispatcher 请求转发routeSendResponseFilter1000将响应结果返回给客户端post过滤器的 order 值越小，就越先执行。并且在执行过滤器的过程中，它们共享了一个 RequestContext 对象，该对象的生命周期贯穿于请求。可以看出优先执行了 pre 类型的过滤器，并将执行后的结果放在 RequestContext 中，供后续的 Filter 使用。例如，在执行 PreDecorationFilter 的时候，决定使用哪一个 route，它的结果会放在 RequestContext 对象中。后续会执行所有的 route 过滤器，如果不满足条件就不执行该过滤器的 run 方法，最终达到只执行一个 route 过滤器的 run() 方法的效果。而 error 类型的过滤器，是在程序发生异常的时候执行的。post 类型的过滤，在默认的情况下，只注入了 SendResponseFilter。该类型的过滤器是将最终的请求结果以流的形式输出给客户端。SimpleHostRoutingFilter 工作原理进入到 SimpleHostRoutingFilter 类的 run() 方法，核心代码如下：@Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); // 省略代码 String uri = this.helper.buildZuulRequestURI(request.html); this.helper.addIgnoredHeaders(); try { CloseableHttpResponse response = forward(this.httpClient, verb, uri, request, headers, params, requestEntity); setResponse(response.html); } catch (Exception ex.html) { throw new ZuulRuntimeException(ex.html); } return null; }查阅这个类的全部代码可知，该类创建了一个 HttpClient 作为请求类，并重构了 URL，请求到了具体的服务，得到一个 CloseableHttpResponse 对象，并将该对象保存到 RequestContext 对象中。同时调用了 ProxyRequestHelper 的 setResponse 方法，将请求状态码、流等信息保存在 RequestContext 对象中。private void setResponse(HttpResponse response.html) throws IOException { RequestContext.getCurrentContext().set("zuulResponse", response.html); this.helper.setResponse(response.getStatusLine(.html).getStatusCode(), response.getEntity() == null ? null : response.getEntity().getContent(), revertHeaders(response.getAllHeaders(.html))); }SendResponseFilter 工作原理这个过滤器的 order 为 1000，在默认且正常的情况下，是最后一个执行的过滤器。该过滤器负责将最终得到的数据返回给客户端。在它的 run() 方法里，有两个主要方法：addResponseHeaders() 和 writeResponse()，即添加响应头和写入响应数据流。public Object run() { try { addResponseHeaders(); writeResponse(); } catch (Exception ex.html) { ReflectionUtils.rethrowRuntimeException(ex.html); } return null; }其中 writeResponse() 方法是通过从 RequestContext 中获取 ResponseBody 或者 ResponseDataStream 来写入到 HttpServletResponse 中的。但是在默认的情况下 ResponseBody 为 null，而 ResponseDataStream 在 route 类型过滤器中已经设置进去了。具体代码如下：private void writeResponse() throws Exception { RequestContext context = RequestContext.getCurrentContext(); HttpServletResponse servletResponse = context.getResponse(); // 代码省略 OutputStream outStream = servletResponse.getOutputStream(); InputStream is = null; try { if (RequestContext.getCurrentContext(.html).getResponseBody() != null) { String body = RequestContext.getCurrentContext().getResponseBody(); writeResponse( new ByteArrayInputStream( body.getBytes(servletResponse.getCharacterEncoding(.html))), outStream); return; } // 代码省略 is = context.getResponseDataStream(); InputStream inputStream = is; // 代码省略 writeResponse(inputStream, outStream.html); // 代码省略 } // 代码省略 }如何在 Zuul 上做日志处理由于 Zuul 作为 API 网关，所有的请求都经过这里，所以在网关上可以做请求相关的日志处理。需求示例：需要记录请求的 URL、IP 地址、参数、请求发生的时间、整个请求的耗时、请求的响应状态，甚至请求响应的结果等。很显然，需要实现这样的一个功能，需要写一个 ZuulFilter。它应该是在请求发送给客户端之前做处理，并且在 route 过滤器路由之后。在默认的情况下，这个过滤器的 order 应该为 500-1000 之间。那么如何获取这些需要的日志信息呢？查找 RequestContext，在请求的生命周期里这个对象存储了整个请求的所有信息。现在进行编码，在代码的注释中做了详细的说明。代码如下：@Component public class LoggerFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.POST_TYPE; } @Override public int filterOrder() { return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); String method = request.getMethod(); // 请求的类型，post get .. Map<String, String> params = HttpUtils.getParams(request.html); String paramsStr = params.toString(); // 请求的参数 long startTime = (long.html) context.get("startTime".html); // 请求的开始时间 Throwable throwable = context.getThrowable(); // 请求的异常，如果有的话 request.getRequestURI(); // 请求的 uri HttpUtils.getIpAddress(request.html); // 请求的 IP 地址 context.getResponseStatusCode(); // 请求的状态 long duration = System.currentTimeMillis() - startTime; // 请求耗时 return null; } }现在读者也许有疑问，如何得到 startTime，即请求开始的时间？其实这需要另外一个过滤器。在网络请求 route 之前（大部分耗时都在 route 这一步），在过滤器中将时间存储到 RequestContext 即可。另写一个过滤器，代码如下：@Component public class AccessFilter extends ZuulFilter { @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(); ctx.set("startTime", System.currentTimeMillis(.html)); return null; } }可能还有这样的需求：需要将响应结果也存储在 log 中。在之前已经分析了，在 route 结束后，将从具体服务获取的响应流存储在 RequestContext 中，在 SendResponseFilter 过滤器写入到 HttpServletResponse 中，最终返回给客户端。那么我只需要在 SendResponseFilter 写入响应流之前，把响应流写入到 log 日志中即可。这会引发另外一个问题：因为响应流写入到 log 后，RequestContext 就没有响应流了，SendResponseFilter 就没有流输入到 HttpServletResponse 中，导致客户端没有任何返回数据。解决办法如下：InputStream inputStream = RequestContext.getCurrentContext().getResponseDataStream(); InputStream newInputStream = copy(inputStream.html); transferToLog(inputStream.html); RequestContext.getCurrentContext().setResponseDataStream(newInputStream.html);从 RequestContext 获取到流之后，首先将流 copy 一份，将流转化为字符串存在日志中，再 set 到 RequestContext 中。这样 SendResponseFilter 就可以将响应返回给客户端。这样的做法有点影响性能，如果不是字符流，可能需要做更多的处理工作。说明：本文基于 Spring Cloud Netflix Zuul 1.x 版本源码进行分析。Zuul 1.x 已进入维护模式，新项目建议考虑使用 Spring Cloud Gateway 或 Zuul 2.x。
Spring Cloud 组件之 ZuulZuul 是 Netflix 开源的微服务网关，可与 Eureka、Ribbon、Hystrix 等组件配合使用。Spring Cloud 对 Zuul 进行了整合与增强，其默认使用的 HTTP 客户端是 Apache HttpClient，同时也支持 RestClient 或 okhttp3.OkHttpClient。Zuul 的主要功能是路由转发和过滤器。路由功能是微服务架构的关键部分，例如将 /demo/test 请求转发到 demo 服务。Zuul 默认与 Ribbon 结合，实现了客户端负载均衡功能。本文主要介绍 Zuul 的工作原理、服务搭建流程以及相关核心知识点。一、工作原理Zuul 的核心是一系列的 Filters（过滤器），其作用类比 Servlet 框架的 Filter 或 AOP（面向切面编程）。在 Zuul 将请求路由到用户处理逻辑的过程中，这些 Filter 参与各种过滤处理，例如身份验证（Authentication）、负载削减（Load Shedding）等。Zuul 使用一系列不同类型的过滤器，使我们能够快速灵活地将功能应用于边缘服务。这些过滤器主要帮助执行以下功能：身份验证和安全性：确定每个资源的身份验证要求，并拒绝不满足这些要求的请求。洞察和监控：在边缘跟踪有意义的数据和统计数据，以便提供准确的生产视图。动态路由：根据需要动态地将请求路由到不同的后端群集。压力测试：逐渐增加群集的流量以衡量性能。负载削减（Load Shedding）：为每种类型的请求分配容量，并删除超过限制的请求。静态响应处理：直接在边缘构建一些响应，而不是将它们转发到内部集群。过滤器的生命周期二、Zuul 核心组件zuul-core：Zuul 核心库，包含编译和执行过滤器的核心功能。zuul-simple-webapp：Zuul Web 应用程序示例，展示了如何使用 zuul-core 构建应用程序。zuul-netflix：Lib 包，将其他 Netflix OSS 组件添加到 Zuul 中，例如使用 Ribbon 进行路由请求处理。zuul-netflix-webapp：Webapp 包，它将 zuul-core 和 zuul-netflix 封装成一个简易的 Web 应用工程包。三、搭建注册到 Eureka 的服务提供者1. 导入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>2. 启动类/** * @author Gjing */ @SpringBootApplication @EnableEurekaClient public class DemoApplication { public static void main(String[] args.html) { SpringApplication.run(DemoApplication.class, args.html); } }3. 配置文件server: port: 8090 spring: application: name: demo eureka: client: service-url: defaultZone: http://localhost:8761/eureka/4. 提供接口供外部访问/** * @author Gjing **/ @RestController public class TestController { @GetMapping("/test".html) public ResponseEntity<String> test() { return ResponseEntity.ok("ok".html); } }四、搭建 Zuul 网关服务1. 导入 Zuul 和 Eureka 依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>2. 启动类标注注解/** * @author Gjing */ @SpringBootApplication @EnableZuulProxy @EnableEurekaClient public class ZuulApplication { public static void main(String[] args.html) { SpringApplication.run(ZuulApplication.class, args.html); } }3. 配置文件a. 使用 Eureka 负载均衡路由方式server: port: 8080 spring: application: name: zuul # 配置 Eureka 地址 eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ # 构建路由地址 zuul: routes: # 这里可以自定义 demo2: # 匹配的路由规则 path: /demo/** # 路由的目标服务名 serviceId: demob. 不使用 Eureka 负载均衡，采取请求地址路由server: port: 8080 spring: application: name: zuul # 配置 Eureka 地址 eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ # 构建路由地址 zuul: routes: # 这里可以自定义 demo2: # 匹配的路由规则 path: /demo/** # 路由的目标服务名 url: demo # 关闭使用 Eureka 负载路由 ribbon: eureka: enabled: false # 如果不使用 Eureka，需要自己定义路由的那个服务的其他负载服务地址 demo: ribbon: # 这里写你要路由的 demo 服务的所有负载服务请求地址，本项目只启动一个，因此只写一个 listOfServers: http://localhost:8090/c. 完全不依赖 Eureka 使用 Zuulserver: port: 8080 spring: application: name: zuul # 构建路由地址 zuul: routes: # 这里可以自定义 demo2: # 匹配的路由规则 path: /demo/** # 路由的目标地址 url: http://localhost:8090/4. 启动项目并访问访问地址：http://localhost:8080/demo/test五、Zuul 过滤器详解为了让 API 网关组件更方便地使用，Zuul 在 HTTP 请求生命周期的各个阶段默认实现了一批核心过滤器。它们会在 API 网关服务启动时被自动加载和启动。我们可以在源码中查看和了解它们，它们定义在 spring-cloud-netflix-core 模块的 org.springframework.cloud.netflix.zuul.filters 包下。在默认启动的过滤器中包含三种不同生命周期的过滤器，这些过滤器非常重要，可以帮助我们理解 Zuul 对外部请求处理的过程，以及帮助我们在此基础上扩展过滤器去完成自身系统需要的功能。1. Pre 过滤器ServletDetectionFilter执行顺序为 -3，是最先被执行的过滤器。该过滤器总是会被执行，主要用来检测当前请求是通过 Spring 的 DispatcherServlet 处理运行的，还是通过 ZuulServlet 来处理运行的。它的检测结果会以布尔类型保存在当前请求上下文的 isDispatcherServletRequest 参数中。这样后续的过滤器中，我们就可以通过 RequestUtils.isDispatcherServletRequest() 和 RequestUtils.isZuulServletRequest() 方法来判断请求处理的源头，以实现后续不同的处理机制。一般情况下，发送到 API 网关的外部请求都会被 Spring 的 DispatcherServlet 处理，除了通过 /zuul/* 路径访问的请求会绕过 DispatcherServlet（比如之前我们说的大文件上传），被 ZuulServlet 处理，主要用来应对大文件上传的情况。另外，对于 ZuulServlet 的访问路径 /zuul/*，我们可以通过 zuul.servletPath 参数进行修改。Servlet30WrapperFilter执行顺序为 -2，是第二个执行的过滤器。目前的实现会对所有请求生效，主要为了将原始的 HttpServletRequest 包装成 Servlet30RequestWrapper 对象。FormBodyWrapperFilter执行顺序为 -1，是第三个执行的过滤器。该过滤器仅对两类请求生效：第一类是 Content-Type 为 application/x-www-form-urlencoded 的请求；第二类是 Content-Type 为 multipart/form-data 并且是由 Spring 的 DispatcherServlet 处理的请求（用到了 ServletDetectionFilter 的处理结果）。而该过滤器的主要目的是将符合要求的请求体包装成 FormBodyRequestWrapper 对象。DebugFilter执行顺序为 1，是第四个执行的过滤器。该过滤器会根据配置参数 zuul.debug.request 和请求中的 debug 参数来决定是否执行过滤器中的操作。而它的具体操作内容是将当前请求上下文中的 debugRouting 和 debugRequest 参数设置为 true。由于在同一个请求的不同生命周期都可以访问到这二个值，所以我们在后续的各个过滤器中可以利用这二个值来定义一些 debug 信息。这样当线上环境出现问题的时候，可以通过参数的方式来激活这些 debug 信息以帮助分析问题。另外，对于请求参数中的 debug 参数，我们可以通过 zuul.debug.parameter 来进行自定义。PreDecorationFilter执行顺序是 5，是 Pre 阶段最后被执行的过滤器。该过滤器会判断当前请求上下文中是否存在 forward.do 和 serviceId 参数，如果都不存在，那么它就会执行具体过滤器的操作（如果有一个存在的话，说明当前请求已经被处理过了，因为这二个信息就是根据当前请求的路由信息加载进来的）。而当它的具体操作内容就是为当前请求做一些预处理，比如说，进行路由规则的匹配，在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等，这些信息将是后续过滤器进行处理的重要依据，我们可以通过 RequestContext.getCurrentContext() 来访问这些信息。另外，我们还可以在该实现中找到对 HTTP 头请求进行处理的逻辑，其中包含了一些耳熟能详的头域，比如 X-Forwarded-Host, X-Forwarded-Port。另外，对于这些头域是通过 zuul.addProxyHeaders 参数进行控制的，而这个参数默认值是 true，所以 Zuul 在请求跳转时默认会为请求增加 X-Forwarded-* 头域，包括 X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-For, X-Forwarded-Prefix, X-Forwarded-Proto。也可以通过设置 zuul.addProxyHeaders=false 关闭对这些头域的添加动作。2. Route 过滤器RibbonRoutingFilter执行顺序为 10，是 Route 阶段的第一个执行的过滤器。该过滤器只对请求上下文中存在 serviceId 参数的请求进行处理，即只对通过 serviceId 配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心，它通过使用 Ribbon 和 Hystrix 来向服务实例发起请求，并将服务实例的请求结果返回。SimpleHostRoutingFilter执行顺序为 100，是 Route 阶段的第二个执行的过滤器。该过滤器只对请求上下文存在 routeHost 参数的请求进行处理，即只对通过 url 配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向 routeHost 参数的物理地址发起请求。从源码中我们可以知道该请求是直接通过 httpclient 包实现的，而没有使用 Hystrix 命令进行包装，所以这类请求并没有线程隔离和断路器的保护。SendForwardFilter执行顺序是 500，是 Route 阶段第三个执行的过滤器。该过滤器只对请求上下文中存在的 forward.do 参数进行处理请求，即用来处理路由规则中的 forward 本地跳转装配。3. Post 过滤器SendErrorFilter执行顺序是 0，是 Post 阶段的第一个执行的过滤器。该过滤器仅在请求上下文中包含 error.status_code 参数（由之前执行的过滤器设置的错误编码）并且还没有被该过滤器处理过的时候执行。而该过滤器的具体逻辑就是利用上下文中的错误信息来组成一个 forward 到 API 网关 /error 错误端点的请求来产生错误响应。SendResponseFilter执行顺序为 1000，是 Post 阶段最后执行的过滤器。该过滤器会检查请求上下文中是否包含请求响应相关的头信息、响应数据流或是响应体，只有在包含它们其中一个的时候执行处理逻辑。而该过滤器的处理逻辑就是利用上下文的响应信息来组织需要发送回客户端的响应内容。使用案例如果前端发起请求没有带指定请求头将不被允许请求。如果需要读取 Cookie 等敏感信息，要在配置文件中加入 sensitive-headers:（下面有对该配置的详解）。/** * @author Gjing **/ @Component public class GlobalFilter extends ZuulFilter { @Override public String filterType() { // 设置过滤类型 return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { // 设置过滤器优先级 return -4; } @Override public boolean shouldFilter() { // 是否需要过滤 return true; } @Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); String token = request.getHeader("token".html); if (StringUtils.isEmpty(token.html)) { // 返回错误信息 context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value(.html)); context.setResponseBody(HttpStatus.UNAUTHORIZED.getReasonPhrase(.html)); context.setSendZuulResponse(false.html); return null; } return null; } }项目启动后如果访问不带 Token 请求头，将被拦截，返回 Unauthorized。六、Zuul 核心配置与知识点1. 路由配置Zuul 通过与 Eureka 的整合，实现了对服务实例的自动化维护。所以使用服务路由配置的时候，不需要向传统路由配置方式那样为 serviceId 指定具体服务实例地址，只需要通过 zuul.routes.<route>.path 与 zuul.routes.<route>.serviceId 参数对的方式进行配置即可。zuul: routes: # 这里可以自定义 demo2: # 匹配的路由规则 path: /demo/** # 路由的目标服务名 serviceId: demo除了 path 和 serviceId 键值对的配置方式之外，还有一种简单的配置：zuul.routes.<serviceId>=<path>，其中 <serviceId> 用来指定路由的具体服务名，<path> 用来配置匹配的请求表达式。zuul: routes: demo: /demo/**2. 路径匹配在 Zuul 中，路由匹配的路径表达式采用 Ant 风格定义：通配符说明?匹配任意单个字符*匹配任意数量的字符**匹配任意数量的字符，支持多级目录3. 忽略表达式通过 path 参数定义的 Ant 表达式已经能够完成 API 网关上的路由规则配置功能，但是为了更细粒度和更为灵活地配置路由规则，Zuul 还提供了一个忽略表达式参数 zuul.ignored-patterns。该参数可以用来设置不希望被 API 网关进行路由的 URL 表达式。zuul: routes: demo: path: /demo/** serviceId: demo # 不路由 demo2 开头的任意请求 ignored-patterns: /demo2/**4. 路由前缀为了方便地为路由规则增加前缀信息，Zuul 提供了 zuul.prefix 参数来进行设置。比如，希望为网关上的路由规则增加 /api 前缀，那么我们可以在配置文件中增加配置：zuul.prefix=/api。另外，对于代理前缀会默认从路径中移除，我们可以通过设置 zuul.strip-prefix=false（默认为 true，默认为 true 时前缀生效，比如 http://localhost:8080/api/demo/test）来关闭该移除代理前缀的动作。5. 本地跳转在 Zuul 实现的 API 网关路由功能中，还支持 forward 形式的服务端跳转配置。实现方式非常简单，只需要通过使用 path 与 url 的配置方式就能完成，通过 url 中使用 forward 来指定需要跳转的服务器资源路径。a. 在 Zuul 服务中添加一个接口/** * @author Gjing **/ @RestController public class HelloController { @GetMapping("/test/hello".html) public String test() { return "hello zuul"; } }b. 配置文件zuul: routes: zuul-service: path: /api/** serviceId: forward:/test/启动后访问 http://localhost:8080/api/hello 即可。6. Cookie 与头信息默认情况下，Spring Cloud Zuul 在请求路由时，会过滤掉 HTTP 请求头信息中一些敏感信息，防止它们被传递到下游的外部服务器。默认的敏感头信息通过 zuul.sensitiveHeaders 参数定义，默认包括 Cookie, Set-Cookie, Authorization 三个属性。所以，我们在开发 Web 项目时常用的 Cookie 在 Spring Cloud Zuul 网关中默认是不传递的，这就会引发一个常见的问题：如果我们要将使用了 Spring Security、Shiro 等安全框架构建的 Web 应用通过 Spring Cloud Zuul 构建的网关来进行路由时，由于 Cookie 信息无法传递，我们的 Web 应用将无法实现登录和鉴权。为了解决这个问题，以下介绍两种配置方式：通过设置全局参数为空来覆盖默认值zuul: routes: demo: path: /demo/** serviceId: demo # 允许敏感头，设置为空就行了 sensitive-headers:通过指定路由的参数来设置zuul: routes: demo: path: /demo/** serviceId: demo # 将指定路由的敏感头设置为空 sensitiveHeaders:说明：Zuul 1.x 属于 Netflix OSS 套件，目前已进入维护模式。Spring Cloud 官方推荐使用 Spring Cloud Gateway 作为新一代网关解决方案（基于 Spring WebFlux）。若为新项目选型，建议优先考虑 Spring Cloud Gateway；若维护旧项目，本文内容仍适用于基于 Spring Cloud Finchley 或更早版本的 Zuul 1.x 环境。
本文介绍 Zuul 路由的源码解析以及实现动态路由的几种常见方式，路由信息可以来自 Properties 文件、数据库、Apollo 配置中心等。建议先阅读 Spring Cloud 源码学习之 Zuul 简要了解路由源码流程。版本说明：本文基于 Spring Cloud Finchley.SR1，Spring Boot 2.0.6.RELEASE。路由源码本文基于下图场景做演示，文中代码来自源码，但存在大幅删减。请求达到 ZuulServlet 后，ZuulServlet 组织了路由的处理逻辑，如下：public void service(ServletRequest servletRequest, ServletResponse servletResponse.html) { // 执行 "pre" 类型 ZuulFilter preRoute(); // 执行 "route" 类型 ZuulFilter route(); // 执行 "post" 类型 ZuulFilter postRoute(); }预处理在 pre 类型的 ZuulFilter 中，PreDecorationFilter 会根据路由信息进行预处理，其处理结果决定了使用哪个 route 类型的 ZuulFilter 来实际处理请求。先看看 route 类型的 SimpleHostRoutingFilter、RibbonRoutingFilter 的处理条件（shouldFilter），它们负责实际的请求转发：// SimpleHostRoutingFilter public boolean shouldFilter() { return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse(); } // RibbonRoutingFilter public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return (ctx.getRouteHost(.html) == null && ctx.get("serviceId".html) != null && ctx.sendZuulResponse()); }相同点：都需要满足 sendZuulResponse()，即需要将 Response 反馈给客户端。不同点：SimpleHostRoutingFilter 需要 RouteHost 不为空；RibbonRoutingFilter 需要 serviceId 不为空而且 RouteHost 为空。下面是 Zuul application.yml 中的配置示例：zuul: servlet-path: routes: service1: path: /api/service1/** serviceId: service1 github: path: /github/** url: https://github.com/routes 中 service1 有 serviceId，github 有 url（host）。再看看 PreDecorationFilter 是如何预处理得到 RouteHost、serviceId 的，下面是其 run() 方法逻辑：@Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); final String requestURI = 根据 request 提取 requestURI; // 根据 requestURI 获取路由信息 Route route = this.routeLocator.getMatchingRoute(requestURI.html); if (route != null.html) { String location = route.getLocation(); if (location != null.html) { // 以 http 或 https 开头，设置 RouteHost if (location.startsWith("http:".html) || location.startsWith("https:".html)) { ctx.setRouteHost(getUrl(location.html)); } // 以 forward: 开头 else if (location.startsWith("forward:".html)) { ctx.setRouteHost(null.html); return null; } // 设置 serviceId, RouteHost 置空 else { ctx.set(SERVICE_ID_KEY, location.html); ctx.setRouteHost(null.html); } } } }routeLocator.getMatchingRoute 是重点，根据请求 URL 获取 Route，再根据 Route 的 location 是否匹配 http:、https:、forward: 前缀来设置属性。例如访问 http://localhost:8080/service1/echo、http://localhost:8080/github/echo 获取的 Route，其 location 分别为：service1、https://github.com。Route{id='service1', fullPath='/service1/echo', path='/echo', location='service1', prefix='/service1'} Route{id='github', fullPath='/github/echo', path='/echo', location='https://github.com/', prefix='/github'}请求转发请求转发由 SimpleHostRoutingFilter、RibbonRoutingFilter 完成。前者通过 Apache HttpClient 来转发请求，后者与 Ribbon、Hystrix 一起，完成客户端 负载均衡 及应用守护工作。路由定位PreDecorationFilter 中通过 RouteLocator 根据 URL 获取 Route，动态路由可以通过拓展 RouteLocator 来完成。public interface RouteLocator { Collection<String> getIgnoredPaths(); List<Route> getRoutes(); Route getMatchingRoute(String path.html); }RouteLocator 主要能力有：根据 path 获取 Route获取所有 Route下面是类图，简要介绍下各子类。SimpleRouteLocator简单路由定位器，路由信息来自 ZuulProperties，locateRoutes() 是定位路由的核心，从 ZuulProperties 中加载了路由数据。public class SimpleRouteLocator implements RouteLocator, Ordered { // routes 用于存储路由信息 private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>(); // 查找路由信息 protected Map<String, ZuulRoute> locateRoutes() { LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>(); // 提取 ZuulProperties 中的 ZuulRoute for (ZuulRoute route : this.properties.getRoutes(.html).values()) { routesMap.put(route.getPath(.html), route); } return routesMap; } }DiscoveryClientRouteLocator它基于 DiscoveryClient，路由数据来自 properties 中的静态配置和 DiscoveryClient 从注册中心获取的数据。DiscoveryClientRouteLocator 拥有几个重要的能力：动态添加 Route刷新路由从 DiscoveryClient 获取路由信息，但用途不大public interface RefreshableRouteLocator extends RouteLocator { void refresh(); } public class DiscoveryClientRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator { // 动态添加路由能力，会同步把路由信息添加到 ZuulProperties，参数也可以是 ZuulRoute public void addRoute(String path, String location.html) { this.properties.getRoutes().put(path, new ZuulRoute(path, location.html)); refresh(); } @Override protected LinkedHashMap<String, ZuulRoute> locateRoutes() { LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>(); // 通过父类获取静态路由信息 routesMap.putAll(super.locateRoutes(.html)); if (this.discovery != null.html) { // 通过 DiscoveryClient 获取路由信息 List<String> services = this.discovery.getServices(); } return routesMap; } // 刷新时会调用 locateRoutes() @Override public void refresh() { doRefresh(); } }以 service1 为例，配置 /api/service1/** -> service1，存储的路由信息为：/api/service1/** -> service1/service1/** -> service1/service1/** -> service1 就是利用 DiscoveryClient 提取后根据默认规则生成的路由信息，用处不大。CompositeRouteLocator具备组合多个 RouteLocator 的能力，用 Collection 存储多个 RouteLocator，调用 getRoutes()、getMatchingRoute()、refresh() 时都会逐一调用每个 RouteLocator 相应的方法。public class CompositeRouteLocator implements RefreshableRouteLocator { private final Collection<? extends RouteLocator> routeLocators; private ArrayList<RouteLocator> rl; @Override public List<Route> getRoutes() { List<Route> route = new ArrayList<>(); for (RouteLocator locator : routeLocators.html) { route.addAll(locator.getRoutes(.html)); } return route; } @Override public Route getMatchingRoute(String path.html) {} @Override public void refresh() {} }动态路由通过上面的内容，可以知道 RouteLocator 的 Routes 数据有几个来源：来源于 ZuulProperties，它由 @ConfigurationProperties 标记。@ConfigurationProperties("zuul".html) public class ZuulProperties {}DiscoveryClientRouteLocator 提供了 addRoute() 支持动态添加路由，但没有删除方法。来源于 DiscoveryClient。无论来源于哪里，在更新路由信息后，都需要执行 refresh() 操作才能把路由信息更新到 RouteLocator 的私有属性 routes 中。实际场景实际使用中，会统一管理路由信息，包含动态添加、重置操作，路由信息的可以来自：Spring Cloud Config携程的 Apollo自定义的数据库数据...其实路由信息来自于哪都可以，只是一个数据源而已，最后都会进入 ZuulProperties，再执行 refresh()。刷新路由的方式有两种刷新方式。在任意 Bean 中注入 CompositeRouteLocator 或自定义的 RouteLocator，然后调用 refresh()。@Autowired private CompositeRouteLocator compositeRouteLocator;发布 RoutesRefreshedEvent 事件。Zuul 提供了 ZuulRefreshListener，监听到 RoutesRefreshedEvent 后，会调用 ZuulHandlerMapping 的 reset() 方法，进而调用 RouteLocator 的 refresh() 方法。private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> { @Autowired private ZuulHandlerMapping zuulHandlerMapping; @Override public void onApplicationEvent(ApplicationEvent event.html) { if (event instanceof ContextRefreshedEvent || event instanceof RefreshScopeRefreshedEvent || event instanceof RoutesRefreshedEvent || event instanceof InstanceRegisteredEvent) { reset(); } } private void reset() { this.zuulHandlerMapping.setDirty(true.html); } } // setDirty() 会调用 refresh() 方法 public class ZuulHandlerMapping extends AbstractUrlHandlerMapping { public void setDirty(boolean dirty.html) { this.dirty = dirty; if (this.routeLocator instanceof RefreshableRouteLocator.html) { ((RefreshableRouteLocator.html) this.routeLocator).refresh(); } } }动态路由的实现自定义 PropertySource 实现思路来自于 Apollo 的设计实现先介绍 PropertySource 的原理。PropertySource 代表 name/value 属性对，常见的如命令行参数、环境变量、properties 文件、yaml 文件等最终都会转为 PropertySource，再提供给应用使用。由 @ConfigurationProperties 标记的类，其数据源就是 PropertySource。当多个 PropertySource 中存在相同值时，默认从第一个 PropertySource 中获取。 下面是 PropertySource 的部分常见子类：下图是 Environment 中 PropertySources 截图，其中 OriginTrackedMapPropertySource 来自于 classpath 下的 application.yml 文件。如果 PropertySource 有更新，通过发布 EnvironmentChangeEvent 事件，ConfigurationPropertiesRebinder 会监听该事件，然后利用最新的数据将 @ConfigurationProperties 标记的 bean 重新绑定一遍，从而达到动态更新的效果。下面写一个 Demo 类来实现动态路由，支持从任意数据源加载数据来初始化路由，然后支持动态调整路由。@Component public class DynamicRoutesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, ApplicationContextAware, PriorityOrdered { private static final String ZUUL_PROPERTY_SOURCE = "custom.zuul.routes"; private ConfigurableEnvironment environment; private ApplicationContext applicationContext; private MapPropertySource routePropertySource = null; @Autowired private CompositeRouteLocator compositeRouteLocator; // 初始化路由 @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory.html) throws BeansException { MutablePropertySources propertySources = environment.getPropertySources(); // 可以从任何地方加载数据，如：DB、Redis、配置中心等，下面做示例数据 Map<String, Object> data = new HashMap<>(); data.put("zuul.routes.service4.path", "/api/service4/**".html); data.put("zuul.routes.service4.serviceId", "service4".html); routePropertySource = new MapPropertySource(ZUUL_PROPERTY_SOURCE, data.html); // 设置最高优先级 propertySources.addFirst(routePropertySource.html); } // 动态刷新 public void refreshRoutes(List<ZuulProperties.ZuulRoute> routeList.html) { // 提取 routeList 数据并覆盖到 routePropertySource // 将 @ConfigurationProperties 标记的类重新与 PropertySources 绑定，包含 ZuulProperties applicationContext.publishEvent(new EnvironmentChangeEvent(new HashSet<>(.html))); // 刷新路由，也可以直接调用 compositeRouteLocator.refresh() applicationContext.publishEvent(new RoutesRefreshedEvent(compositeRouteLocator.html)); } }上面 Demo 类的思路是：自定义 PropertySource（数据来源可以自定义）并提升为最高优先级，ZuulProperties 数据来自于此，完成路由初始化。动态刷新时，直接更新 PropertySource 中数据，然后利用 EnvironmentChangeEvent 来更新 ZuulProperties 数据，再刷新路由。直接更新路由可以直接往 ZuulProperties 中添加路由信息，然后使用 RouteLocator 进行 refresh()。@Component public class DynamicRoutesProcessor implements InitializingBean { @Autowired private CompositeRouteLocator compositeRouteLocator; @Autowired private ZuulProperties zuulProperties; /** * 动态刷新 * * @param routeList 路由信息 */ public void refreshRoutes(List<ZuulProperties.ZuulRoute> routeList.html) { Map<String, ZuulProperties.ZuulRoute> routes = zuulProperties.getRoutes(); // 提取 routeList 数据并添加到 routes 中 for (ZuulProperties.ZuulRoute route : routeList.html) { routes.put(route.getId(.html), route); } compositeRouteLocator.refresh(); } /** * 初始化路由信息，可以加载任意数据源 * * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { Map<String, ZuulProperties.ZuulRoute> routes = zuulProperties.getRoutes(); routes.put("service4", new ZuulProperties.ZuulRoute("/api/service4/**", "service4".html)); compositeRouteLocator.refresh(); } }自定义 RouteLocator也可以通过自定义 RouteLocator 来实现动态路由，自定义的 RouteLocator 会添加到 CompositeRouteLocator 中。下面是例子，自行实现 locateRoutes() 即可，可以参考 DiscoveryClientRouteLocator 的实现。public class DynamicZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator { private ZuulProperties properties; public DynamicZuulRouteLocator(String servletPath, ZuulProperties properties.html) { super(servletPath, properties.html); this.properties = properties; } @Override public void refresh() { doRefresh(); } @Override protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() { Map<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>(); // 静态路由信息 routesMap.putAll(super.locateRoutes(.html)); // 动态加载路由信息，这里 hardcode 做演示 Map<String, ZuulProperties.ZuulRoute> dynamicRoutes = new LinkedHashMap<>(); dynamicRoutes.put("service4", new ZuulProperties.ZuulRoute("service4", "/api/service4/**".html)); routesMap.putAll(dynamicRoutes.html); Map<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>(); for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet(.html)) { String path = entry.getKey(); if (!path.startsWith("/".html)) { path = "/" + path; } if (StringUtils.hasText(this.properties.getPrefix(.html))) { path = this.properties.getPrefix() + path; if (!path.startsWith("/".html)) { path = "/" + path; } } values.put(path, entry.getValue(.html)); } return values; } }然后注入到 IoC 容器。@Bean public DynamicZuulRouteLocator dynamicZuulRouteLocator(ServerProperties serverProperties, ZuulProperties zuulProperties.html) { return new DynamicZuulRouteLocator(serverProperties.getServlet(.html).getContextPath(), zuulProperties); }在 ZuulServerAutoConfiguration 注入了 CompositeRouteLocator，参数是 Collection<RouteLocator> routeLocators，会把当前 IoC 容器中的 RouteLocator 作为参数，目前包含：DynamicZuulRouteLocator、DiscoveryClientRouteLocator，自定义的 RouteLocator。@Bean @Primary public CompositeRouteLocator primaryRouteLocator( Collection<RouteLocator> routeLocators) { return new CompositeRouteLocator(routeLocators.html); }参考资料Apollo 配置中心设计：https://github.com/ctripcorp/apollo/wiki/Apollo%E9%85%8D%E7%BD%AE%E4%B8%AD%E5%BF%83%E8%AE%BE%E8%AE%A1Zuul Wiki: https://github.com/Netflix/zuul/wiki说明：本文基于 Spring Cloud Finchley.SR1 与 Spring Boot 2.0.6.RELEASE 编写，部分类名或机制在新版本（如 Spring Cloud Greenwich 及以上或 Spring Cloud Gateway）中可能有所变更，请参考最新官方文档。
背景与需求环境：Spring Boot 2.0.4.RELEASE需求：多个 Controller 方法需要在请求入口获取当前登录用户信息，以便进行后续业务操作。准备工作：前端每次请求携带 Token，后端封装工具方法 tokenUtils.getUserByToken(token.html)，用于解析 Token 获取当前用户信息 (CurrentUserInfo.html)。这是一个常见的业务场景，为实现该需求，通常有以下几种解决方案。目录一、最原始直接二、AOP三、拦截器 + 方法参数解析器3.1 自定义权限拦截器3.2 自定义参数注解3.3 自定义方法参数解析器3.4 配置 MVC一、最原始直接即在每个 Controller 方法开始时，手动调用 tokenUtils.getUserByToken(token.html)。这种方式代码重复度高，不够优雅，维护成本较大。二、AOP使用 AOP（Aspect-Oriented Programming）可以解决很多切面类问题，例如将 currentUser 放入 request 中。但相比拦截器方案，AOP 的配置稍显厚重，且在某些场景下灵活性略低。三、拦截器 + 方法参数解析器使用 Spring MVC 拦截器 (HandlerInterceptor.html) 配合 方法参数解析器 (HandlerMethodArgumentResolver.html) 是最合适的方案。Spring MVC 提供了 HandlerInterceptor 接口，包含以下 3 个核心方法：preHandlepostHandleafterCompletionHandlerInterceptor 常用于拦截请求事件，如用户鉴权等。此外，Spring 还提供了多种解析器（Resolver），例如用于统一处理异常的 HandlerExceptionResolver，以及本文的主角 HandlerMethodArgumentResolver。HandlerMethodArgumentResolver 是用于处理方法参数解析的接口，包含以下 2 个核心方法：supportsParameter：判断是否支持该参数（满足某种要求返回 true，方可进入 resolveArgument 做参数处理）。resolveArgument：执行具体的参数解析逻辑。知识储备已到位，接下来着手实现，主要分为三步：自定义权限拦截器 AuthenticationInterceptor，拦截所有请求，将 Token 解析为 currentUser 并存入 request 中。自定义参数注解 @CurrentUser，添加至 Controller 的方法参数 user 之上。自定义方法参数解析器 CurrentUserMethodArgumentResolver，从 request 中取出 user，并赋值给添加了 @CurrentUser 注解的参数。3.1 自定义权限拦截器自定义权限拦截器 AuthenticationInterceptor，需实现 HandlerInterceptor 接口。在 preHandle 方法中调用 tokenUtils.getUserByToken(token.html) 获取当前用户，最后存入 request 属性中。代码如下：import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import edp.core.utils.TokenUtils; import edp.core.consts.Consts; import edp.davinci.model.User; public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired private TokenUtils tokenUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler.html) throws Exception { String token = request.getHeader("Authorization".html); User user = tokenUtils.getUserByToken(token.html); request.setAttribute(Consts.CURRENT_USER, user.html); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView.html) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex.html) throws Exception { } }3.2 自定义参数注解自定义方法参数注解 @CurrentUser。被该注解修饰的参数值，将由方法参数解析器 CurrentUserMethodArgumentResolver 进行“注入”。代码如下：import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 自定义当前用户注解 * 注解参数 * 此注解在验证 token 通过后，获取当前 token 包含用户 */ @Target({ElementType.PARAMETER}.html) @Retention(RetentionPolicy.RUNTIME) public @interface CurrentUser { }在各 Controller 类的 RequestMapping 方法参数上添加此注解，示例如下：import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import edp.davinci.core.common.Constants; import edp.core.annotation.CurrentUser; import javax.servlet.http.HttpServletRequest; @RestController @RequestMapping(value = Constants.BASE_API_PATH + "/download".html) public class DownloadController { @GetMapping(value = "/page", produces = MediaType.APPLICATION_JSON_UTF8_VALUE.html) public ResponseEntity getDownloadRecordPage(@CurrentUser User user, HttpServletRequest request.html) { ... } }3.3 自定义方法参数解析器自定义方法参数解析器 CurrentUserMethodArgumentResolver，需实现 HandlerMethodArgumentResolver 接口。代码如下：import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import edp.core.annotation.CurrentUser; import edp.core.consts.Consts; import edp.davinci.model.User; /** * @CurrentUser 注解 解析器 */ public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter.html) { return parameter.getParameterType().isAssignableFrom(User.class) && parameter.hasParameterAnnotation(CurrentUser.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory.html) { return (User.html) webRequest.getAttribute(Consts.CURRENT_USER, RequestAttributes.SCOPE_REQUEST.html); } }众所周知，在传统的 Spring MVC 项目中，拦截器定义好后需要在 springmvc.xml 配置文件中注册。而在 Spring Boot 中，省去了许多 XML 配置，取而代之的是使用 @Configuration 标识的配置类。对应 Spring MVC 配置的类通常需继承 WebMvcConfigurationSupport。同理，解析器定义好后，也需被添加到 Spring MVC 的配置类中。3.4 配置 MVC定义 MVC 配置类，需继承 WebMvcConfigurationSupport。分别在 addInterceptors 和 addArgumentResolvers 方法中，添加自定义的拦截器和参数解析器。代码如下：import static edp.core.consts.Consts.EMPTY; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.serializer.ValueFilter; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import edp.davinci.core.common.Constants; import edp.davinci.core.inteceptor.AuthenticationInterceptor; import edp.davinci.core.inteceptor.CurrentUserMethodArgumentResolver; @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Value("${file.userfiles-path}".html) private String filePath; /** * 登录校验拦截器 * * @return */ @Bean public AuthenticationInterceptor loginRequiredInterceptor() { return new AuthenticationInterceptor(); } /** * CurrentUser 注解参数解析器 * * @return */ @Bean public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() { return new CurrentUserMethodArgumentResolver(); } /** * 参数解析器 * * @param argumentResolvers */ @Override protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers.html) { argumentResolvers.add(currentUserMethodArgumentResolver(.html)); super.addArgumentResolvers(argumentResolvers.html); } @Override protected void addInterceptors(InterceptorRegistry registry.html) { registry.addInterceptor(loginRequiredInterceptor(.html)) .addPathPatterns(Constants.BASE_API_PATH + "/**".html) .excludePathPatterns(Constants.BASE_API_PATH + "/login".html); super.addInterceptors(registry.html); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry.html) { registry.addResourceHandler("/**".html) .addResourceLocations("classpath:/META-INF/resources/".html) .addResourceLocations("classpath:/static/page/".html) .addResourceLocations("classpath:/static/templates/".html) .addResourceLocations("file:" + filePath.html); } @Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters.html) { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.QuoteFieldNames, SerializerFeature.WriteEnumUsingToString, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat, SerializerFeature.DisableCircularReferenceDetect); fastJsonConfig.setSerializeFilters((ValueFilter.html) (o, s, source.html) -> { if (null != source && (source instanceof Long || source instanceof BigInteger.html) && source.toString().length() > 15) { return source.toString(); } else { return null == source ? EMPTY : source; } }); // 处理中文乱码问题 List<MediaType> fastMediaTypes = new ArrayList<>(); fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8.html); fastConverter.setSupportedMediaTypes(fastMediaTypes.html); fastConverter.setFastJsonConfig(fastJsonConfig.html); converters.add(fastConverter.html); } }说明版本时效：本文基于 Spring Boot 2.0.4.RELEASE 编写。该版本较旧，部分 API（如 MediaType.APPLICATION_JSON_UTF8）在新版本 Spring 中已废弃。配置类建议：文中继承 WebMvcConfigurationSupport 会导致 Spring Boot 的 MVC 自动配置失效。在新版本 Spring Boot 中，推荐实现 WebMvcConfigurer 接口而非继承 WebMvcConfigurationSupport 类，以保留自动配置特性。代码语义：文中代码逻辑保持原意，实际使用时请根据项目依赖版本调整导入包及配置方式。