Sentinel 无感接入架构(ByteBuddy 方案)

本文档详细阐述 ByteBuddy 自研 Agent 方案下 Sentinel 的无感接入架构,包括类加载隔离与桥接机制、各框架流控实现,以及与官方 Adapter 的差异点。


一、Sentinel Core 与 Adapter 类加载示意

1.1 ClassLoader 全景图

类的加载、隔离、桥接

1.2 Agent 目录布局

sentinel-agent/
├── sentinel-agent-core.jar                 # -javaagent 指向此文件(含 shaded Byte Buddy)
├── bootstrap/
│   └── sentinel-agent-bootstrap.jar        # 注入 BootstrapCL(SentinelClassBridge + SentinelAutoInjector)
├── lib/                                    # PluginClassLoader 的 classpath
│   ├── sentinel-core-1.8.6.jar
│   ├── sentinel-transport-common-1.8.6.jar
│   └── fastjson-xxx.jar ...
└── adapters/                               # 各框架 Adapter JAR(运行时 addURL 到 App CL)
    ├── sentinel-apache-dubbo-adapter-1.8.6.jar
    ├── sentinel-mybatis-adapter.jar
    ├── sentinel-adapter-spring-webmvc-1.8.6.jar
    ├── sentinel-apache-httpclient-adapter-1.8.6.jar
    ├── sentinel-okhttp-adapter-1.8.6.jar
    └── spring-cloud-starter-alibaba-sentinel-2022.0.0.0.jar  # Feign + RestTemplate

1.3 premain 启动流程

// SentinelAgent.premain()
public static void premain(String args, Instrumentation inst) {
    // 1. 注入 bootstrap JAR → BootstrapCL
    injectBootstrapJar(agentDir, inst);
    // 2. 创建隔离 PluginCL (parent=null),加载 sentinel-core
    URLClassLoader pluginCl = createPluginClassLoader(agentDir);
    // 3. 初始化 ClassBridge —— 注册桥接前缀 "com.alibaba.csp.sentinel."
    initClassBridge(pluginCl);
    // 4. 设置 adapter JAR 路径为系统属性
    setAdapterPaths(agentDir);
    // 5. 预初始化 Sentinel SPI (TCCL 临时设为 PluginCL)
    preInitSentinel(pluginCl);
    // 6. 安装 Byte Buddy 字节码增强(10 个 Transformer)
    installTransformers(inst);
}

1.4 桥接机制 —— 类加载拦截与路由

Byte Buddy Advice 内联到 ClassLoader.loadClass(String, boolean) 方法入口:

// ClassLoaderLoadClassAdvice.java —— 内联到所有 ClassLoader 的 loadClass
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
public static Class<?> onEnter(@Advice.This ClassLoader self,
                                @Advice.Argument(0) String name) {
    return SentinelClassBridge.loadClass(name, self);
}

@Advice.OnMethodExit
public static void onExit(@Advice.Enter Class<?> bridgedClass,
                           @Advice.Return(readOnly = false) Class<?> returnValue) {
    if (bridgedClass != null) {
        returnValue = bridgedClass;
    }
}
// SentinelClassBridge.java —— 在 BootstrapCL 中,全局可见
public static Class<?> loadClass(String name, ClassLoader caller) {
    if (name == null || pluginClassLoader == null) return null;
    if (name.startsWith("java.") || name.startsWith("sun.") || name.startsWith("jdk.")) return null;
    if (caller == pluginClassLoader) return null;  // 防递归
    for (String prefix : classPrefixes) {           // "com.alibaba.csp.sentinel."
        if (name.startsWith(prefix)) {
            try { return pluginClassLoader.loadClass(name); }
            catch (ClassNotFoundException e) { return null; }
        }
    }
    return null;
}

桥接流程四步:

步骤 动作 说明
AppCL.loadClass("com.alibaba.csp.sentinel.SphU") Adapter/业务代码引用 Sentinel 类
Byte Buddy 内联代码 → SentinelClassBridge.loadClass(name, caller) loadClass 方法入口拦截
name.startsWith("com.alibaba.csp.sentinel.")pluginCL.loadClass(name) 前缀匹配 → 委托 PluginCL 加载
skipOn 生效 → onExit: returnValue = bridgedClass 替换返回值,跳过原方法体

1.5 Adapter "双头依赖" 解决路径

Adapter 类同时依赖 Sentinel Core框架类(如 org.apache.dubbo.rpc.Filter),两者位于不同 ClassLoader。解决方案:

  1. Adapter JAR 通过 addURL() 注入到 应用 ClassLoader
  2. Adapter 类由 App CL 定义
  3. 引用 Sentinel Core → App CL.loadClass("SphU")Bridge 拦截PluginCL.loadClass → 找到 ✔
  4. 引用框架类 → App CL.loadClass("Filter")直接找到

Adapter JAR 不在 PluginCL 的 lib/ 中,Bridge 尝试加载时 CNFE → fallback 给 App CL 继续加载,确保 Adapter 由 App CL 定义。


二、HTTP 服务端流控 —— Spring WebMVC

2.1 整体方案

Spring WebMVC的流控
维度 官方接入方式 无感接入方式
触发机制 Spring Boot AutoConfiguration Byte Buddy Advice 拦截 InterceptorRegistry.getInterceptors()
Adapter JAR sentinel-spring-webmvc-adapter(Maven 依赖引入) 同一 JAR(运行时 addURL 注入 App CL)
拦截器注册 SentinelWebMvcConfigurer.addInterceptors() → Bean 方式注册 直接 interceptorList.add(0, webInterceptor) 插入到列表首位
配置方式 SentinelWebMvcConfig Bean + application.yml 属性 使用 SentinelWebInterceptor 默认构造器(无配置)

2.2 官方接入方式

spring-cloud-starter-alibaba-sentinel 中,SentinelWebAutoConfiguration 自动注册:

// SentinelWebAutoConfiguration.java(官方 Spring Cloud Alibaba)
@Configuration
@ConditionalOnWebApplication(type = SERVLET)
@ConditionalOnClass(SentinelWebInterceptor.class)
public class SentinelWebAutoConfiguration {

    @Bean
    public SentinelWebInterceptor sentinelWebInterceptor(SentinelWebMvcConfig config) {
        return new SentinelWebInterceptor(config);  // ← 传入配置对象
    }

    @Bean
    public SentinelWebMvcConfig sentinelWebMvcConfig() {
        SentinelWebMvcConfig config = new SentinelWebMvcConfig();
        config.setHttpMethodSpecify(properties.getHttpMethodSpecify());
        config.setWebContextUnify(properties.getWebContextUnify());
        // UrlCleaner, BlockExceptionHandler, RequestOriginParser 等可自定义
        return config;
    }

    @Bean
    public SentinelWebMvcConfigurer sentinelWebMvcConfigurer() {
        // 通过 WebMvcConfigurer.addInterceptors() 注册
        // 可配置 order 和 urlPatterns
    }
}

官方 SentinelWebInterceptor 的流控核心(继承自 AbstractSentinelInterceptor):

// AbstractSentinelInterceptor.preHandle()
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    String resourceName = getResourceName(request);
    // 引用计数:防止 forward/include 重复流控
    if (increaseReference(request, requestRefName, 1) != 1) return true;
    String origin = parseOrigin(request);   // RequestOriginParser
    String contextName = getContextName(request);
    ContextUtil.enter(contextName, origin);
    Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
    request.setAttribute(requestAttributeName, entry);
    return true;
    // catch BlockException → blockExceptionHandler.handle(request, response, resourceName, e)
}

资源名提取(SentinelWebInterceptor):

protected String getResourceName(HttpServletRequest request) {
    // 使用 HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE(如 /api/users/{id})
    Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
    String resourceName = (String) resourceNameObject;
    // 可选:UrlCleaner 清洗
    // 可选:httpMethodSpecify → "GET:/api/users/{id}"
    return resourceName;
}

2.3 无感接入方式

拦截点: InterceptorRegistry.getInterceptors() 方法返回后

// SpringWebMvcGetInterceptorsAdvice.java
public class SpringWebMvcGetInterceptorsAdvice {
    @Advice.OnMethodExit
    public static void onExit(@Advice.This Object interceptorRegistry,
                               @Advice.Return Object interceptors) {
        SentinelAutoInjector.afterGetInterceptors(interceptorRegistry, interceptors);
    }
}

注入逻辑:

// SentinelAutoInjector.afterGetInterceptors()
public static void afterGetInterceptors(Object interceptorRegistry, Object interceptors) {
    List<Object> interceptorList = (List<Object>) interceptors;

    // 1. 幂等检查:已存在 SentinelWebInterceptor 则跳过
    for (Object interceptor : interceptorList) {
        if (interceptor.getClass().getName().contains("SentinelWebInterceptor")) return;
    }

    // 2. addURL 注入 sentinel-spring-webmvc-adapter JAR 到 App CL
    injectSpringWebMvcAdapter(appCl);

    // 3. 反射创建 SentinelWebInterceptor(默认构造器)
    Class<?> interceptorClass = Class.forName(SENTINEL_WEB_INTERCEPTOR_CLASS, true, appCl);
    Object webInterceptor = interceptorClass.getDeclaredConstructor().newInstance();

    // 4. 插入到拦截器列表首位
    interceptorList.add(0, webInterceptor);
}

2.4 ⚠️ 与官方 Adapter 的差异

差异点 官方方式 无感接入 影响
SentinelWebMvcConfig 通过 Bean 注入配置(UrlCleanerBlockExceptionHandlerRequestOriginParserhttpMethodSpecify 等) 使用默认构造器,无配置 无法自定义 URL 清洗、来源解析、HTTP 方法前缀等
拦截器 Order 可通过 spring.cloud.sentinel.filter.order 配置(默认 HIGHEST_PRECEDENCE 固定插入列表首位 (add(0, ...)),无 Order 概念 与其他 HandlerInterceptor 的执行顺序可能不同
URL Patterns 可配置 spring.cloud.sentinel.filter.urlPatterns(默认 ["/**"] 拦截所有请求,无法配置路径过滤 无法排除健康检查等非业务路径
BlockException 处理 自定义 BlockExceptionHandler Bean(默认返回 429) 使用默认 handler(返回 429,DefaultBlockExceptionHandler 无法自定义限流返回内容
webContextUnify 可配置,控制是否统一 Web 上下文 使用默认值 (true) 影响链路模式限流
引用计数(forward/include) 框架内置引用计数,防止子请求重复流控 一致AbstractSentinelInterceptor 内置引用计数机制 无差异
资源名 HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE(如 /api/users/{id} 一致,复用同一个 SentinelWebInterceptor 无差异

核心影响: 无感接入场景下 SentinelWebInterceptor 以默认配置运行,用户无法通过 application.yml 自定义 URL 清洗器、限流返回体、来源解析器等。如需精细配置,需额外实现配置注入逻辑。


三、HTTP 客户端流控

3.0 总览

客户端 注入方式 注入的 Adapter 拦截点 资源名格式
RestTemplate Adapter JAR 注入 spring-cloud-starter-alibaba-sentinel 构造器 + setInterceptors(List) GET:http://host:port/path
Apache HttpClient Adapter JAR 注入 sentinel-apache-httpclient-adapter HttpClientBuilder.decorateMainExec() httpclient:/path
OkHttp Adapter JAR 注入 sentinel-okhttp-adapter OkHttpClient.Builder.build() okhttp:GET:http://host/path
Feign Adapter JAR 注入 spring-cloud-starter-alibaba-sentinel Feign.Builder.build() GET:http://service/path
HttpURLConnection 直接流控(无 Adapter) connect() + getInputStream() HttpURLConnection_GET_/path

3.1 RestTemplate 流控

RestTemplate_Adapter注入模式

3.1.1 官方接入方式

官方依赖 @SentinelRestTemplate 注解 + SentinelBeanPostProcessor 自动装配:

// 用户代码 —— 必须显式标注 @SentinelRestTemplate
@Bean
@SentinelRestTemplate(blockHandler = "handleBlock", blockHandlerClass = MyHandler.class,
                       fallback = "handleFallback", fallbackClass = MyHandler.class,
                       urlCleaner = "cleanUrl", urlCleanerClass = MyHandler.class)
public RestTemplate restTemplate() {
    return new RestTemplate();
}

SentinelBeanPostProcessorpostProcessAfterInitialization 中扫描带 @SentinelRestTemplate 注解的 Bean,将 SentinelProtectInterceptor 添加到其拦截器链。

SentinelProtectInterceptor 的双资源策略:

// SentinelProtectInterceptor.intercept()
// 资源 1(主机级): GET:http://service-a:8080
Entry hostEntry = SphU.entry(hostResource, EntryType.OUT);
// 资源 2(路径级): GET:http://service-a:8080/api/users
Entry hostWithPathEntry = SphU.entry(hostWithPathResource, EntryType.OUT);
// 执行实际请求
response = execution.execute(request, body);
// BlockException 处理: 熔断走 fallback,限流走 blockHandler,默认返回 SentinelClientHttpResponse

3.1.2 无感接入方式

拦截点: RestTemplate 所有构造器 + setInterceptors(List) 方法

// RestTemplateConstructorAdvice.java
@Advice.OnMethodExit
public static void onExit(@Advice.This Object restTemplate) {
    SentinelAutoInjector.ensureSentinelProtectInterceptorOnRestTemplate(restTemplate);
}

// RestTemplateSetInterceptorsAdvice.java
@Advice.OnMethodExit
public static void onExit(@Advice.This Object restTemplate) {
    SentinelAutoInjector.ensureSentinelProtectInterceptorOnRestTemplate(restTemplate);
}

注入逻辑核心:

// SentinelAutoInjector.ensureSentinelProtectInterceptorOnRestTemplate()
public static void ensureSentinelProtectInterceptorOnRestTemplate(Object restTemplate) {
    // 1. addURL 注入 spring-cloud-starter-alibaba-sentinel JAR
    injectRestTemplateAdapter(appCl);

    // 2. 通过 JDK 动态代理创建默认的 @SentinelRestTemplate 注解实例
    //    blockHandler=""、fallback=""、urlCleaner="" —— 全部为空
    Object annotation = createDefaultSentinelRestTemplateAnnotation(annotationClass, appCl);

    // 3. 反射创建 SentinelProtectInterceptor(annotation, restTemplate)
    Object interceptor = interceptorCtor.newInstance(annotation, restTemplate);

    // 4. 确保 SentinelProtectInterceptor 位于拦截器链首位
    interceptors.add(0, interceptor);
}

动态代理注解关键实现:

// 所有自定义属性返回空值 —— 不支持 blockHandler/fallback/urlCleaner
public Object invoke(Object proxy, Method method, Object[] args) {
    if ("blockHandler".equals(methodName) || "fallback".equals(methodName)
            || "urlCleaner".equals(methodName)) {
        return "";    // ← 空字符串,不会触发自定义处理
    }
    if ("blockHandlerClass".equals(methodName) || "fallbackClass".equals(methodName)
            || "urlCleanerClass".equals(methodName)) {
        return Void.TYPE;  // ← 无处理类
    }
    // ...
}

3.1.3 ⚠️ 与官方 Adapter 的差异

差异点 官方方式 无感接入 影响
覆盖范围 @SentinelRestTemplate 注解的 Bean 所有 RestTemplate 实例(包括无注解的) 非业务 RestTemplate(如框架内部用)也会被流控
blockHandler 可指定类和方法处理限流 不支持(注解属性为空) 限流时使用默认 SentinelClientHttpResponse
fallback 可指定类和方法处理熔断 不支持(注解属性为空) 熔断时使用默认 SentinelClientHttpResponse
urlCleaner 可配置 URL 清洗逻辑 不支持(注解属性为空) 无法归并 RESTful 路径变量(如 /users/123/users/{id}
双资源策略 ✅ 主机级 + 路径级 一致,复用 SentinelProtectInterceptor 无差异
资源名格式 METHOD:scheme://host[:port]/path 一致 无差异
注入时机 Spring Bean 后置处理(postProcessAfterInitialization 构造器退出时 + setInterceptors 退出时 更早注入,确保不被后续 setInterceptors 覆盖

3.2 Apache HttpClient 流控

Apache HttpClient Adapter 注入模式

3.2.1 官方接入方式

官方要求用户替换 HttpClientBuilderSentinelApacheHttpClientBuilder

// 官方用法 —— 需要修改代码
CloseableHttpClient httpClient = new SentinelApacheHttpClientBuilder()
    .build();

SentinelApacheHttpClientBuilder 重写 decorateMainExec(ClientExecChain),用匿名 ClientExecChain 包装原始链:

// SentinelApacheHttpClientBuilder.decorateMainExec()
@Override
protected ClientExecChain decorateMainExec(final ClientExecChain mainExec) {
    return new ClientExecChain() {
        public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request, ...) {
            String name = config.getExtractor().extractor(request);   // 默认: request.getRequestLine().getUri()
            if (!StringUtil.isEmpty(config.getPrefix())) {
                name = config.getPrefix() + name;                     // 默认前缀: "httpclient:"
            }
            Entry entry = SphU.entry(name, ResourceTypeConstants.COMMON_WEB, EntryType.OUT);
            return mainExec.execute(route, request, clientContext, execAware);
            // catch BlockException → config.getFallback().handle(request, e)
            // finally: entry.exit()
        }
    };
}

3.2.2 无感接入方式

拦截点: HttpClientBuilder.decorateMainExec(ClientExecChain) 返回后

// HttpClientDecorateMainExecAdvice.java
@Advice.OnMethodExit
public static void onExit(@Advice.This Object builder,
                           @Advice.Return(readOnly = false) Object returnValue) {
    returnValue = SentinelAutoInjector.wrapHttpClientExecChain(returnValue, builder);
}

注入逻辑:

// SentinelAutoInjector.wrapHttpClientExecChain()
public static Object wrapHttpClientExecChain(Object originalChain, Object builder) {
    // 1. 幂等检查:已经是 SentinelApacheHttpClientBuilder 则跳过
    if (builder.getClass().getName().contains("SentinelApacheHttpClientBuilder")) return originalChain;

    // 2. addURL 注入 sentinel-apache-httpclient-adapter JAR 到 App CL
    injectHttpClientAdapter(appCl);

    // 3. 反射创建 SentinelApacheHttpClientBuilder 实例
    // 4. 调用其 decorateMainExec(originalChain) 包装原始执行链
    Object wrappedChain = holder.decorateMainExecMethod.invoke(holder.sentinelBuilder, originalChain);
    return wrappedChain;
}

设计类比:

TTL Agent:       $1 = TtlTransformletHelper.doAutoWrap($1)   → 包装 Runnable 参数
HttpClient Agent: ret = wrapHttpClientExecChain(ret, builder) → 包装 ClientExecChain 返回值

3.2.3 ⚠️ 与官方 Adapter 的差异

差异点 官方方式 无感接入 影响
接入方式 需将 HttpClientBuilder 替换为 SentinelApacheHttpClientBuilder 零修改,自动包装所有 HttpClientBuilder 完全透明
覆盖范围 仅手动替换的 Builder 所有 HttpClientBuilder.build() 创建的客户端 非业务 HttpClient 也被流控(如框架内部 HTTP 调用)
SentinelApacheHttpClientConfig 可自定义 prefixextractorfallback 使用默认配置 资源前缀固定 httpclient:,提取器固定 getRequestLine().getUri()
Fallback 可自定义 ApacheHttpClientFallback 默认 DefaultApacheHttpClientFallback(抛 SentinelRpcException 限流异常类型为 SentinelRpcException
资源名 httpclient: + URI 一致 无差异
EntryType OUT 一致 无差异

3.3 OkHttp 流控

OkHttp_Adapter注入模式

3.3.1 官方接入方式

官方要求手动添加 Interceptor:

// 官方用法 —— 需要修改代码
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new SentinelOkHttpInterceptor())  // ← 手动添加
    .build();

SentinelOkHttpInterceptor 作为 Application Interceptor,同时覆盖同步 execute() 和异步 enqueue()

// SentinelOkHttpInterceptor.intercept()
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    String name = config.getResourceExtractor().extract(request, chain.connection());
    // 默认: request.method() + ":" + request.url().toString()
    if (StringUtil.isNotBlank(config.getResourcePrefix())) {
        name = config.getResourcePrefix() + name;   // 默认前缀: "okhttp:"
    }
    Entry entry = SphU.entry(name, ResourceTypeConstants.COMMON_WEB, EntryType.OUT);
    return chain.proceed(request);
    // catch BlockException → config.getFallback().handle(request, connection, e)
    // finally: entry.exit()
}

3.3.2 无感接入方式

拦截点: OkHttpClient.Builder.build() 方法入口

// OkHttpClientBuildAdvice.java
@Advice.OnMethodEnter
public static void onEnter(@Advice.This Object builder) {
    SentinelAutoInjector.beforeOkHttpClientBuild(builder);
}

注入逻辑:

// SentinelAutoInjector.beforeOkHttpClientBuild()
public static void beforeOkHttpClientBuild(Object builder) {
    // 1. addURL 注入 sentinel-okhttp-adapter JAR 到 App CL
    injectOkHttpAdapter(appCl);

    // 2. 幂等检查:反射读取 Builder.interceptors 列表,已存在 SentinelOkHttpInterceptor 则跳过
    List<Object> interceptors = (List<Object>) interceptorsField.get(builder);
    for (Object interceptor : interceptors) {
        if (interceptor.getClass().getName().equals(SENTINEL_OKHTTP_INTERCEPTOR_CLASS)) return;
    }

    // 3. 反射创建 SentinelOkHttpInterceptor(默认构造器)
    Object sentinelInterceptor = interceptorClass.getDeclaredConstructor().newInstance();

    // 4. 调用 builder.addInterceptor(sentinelInterceptor)
    addInterceptor.invoke(builder, sentinelInterceptor);
}

3.3.3 ⚠️ 与官方 Adapter 的差异

差异点 官方方式 无感接入 影响
接入方式 手动 addInterceptor(new SentinelOkHttpInterceptor()) 零修改,自动注入到所有 Builder 完全透明
覆盖范围 仅手动添加的 Builder 所有 OkHttpClient.Builder.build() 非业务 OkHttp 也被流控
SentinelOkHttpConfig 可自定义 resourcePrefixresourceExtractorfallback 使用默认配置 前缀固定 okhttp:,提取器固定 method:url
Fallback 可自定义 OkHttpFallback 默认 DefaultOkHttpFallback(抛 SentinelRpcException 限流异常为 SentinelRpcException
资源名 okhttp: + method:url 一致 无差异
同步/异步覆盖 ✅ 同步 execute + 异步 enqueue 一致,Application Interceptor 天然覆盖两者 无差异

3.4 Feign 流控

Feign_Adapter注入模式

3.4.1 官方接入方式

官方通过 Spring Boot AutoConfiguration:

# application.yml
feign:
  sentinel:
    enabled: true   # ← 触发 SentinelFeignAutoConfiguration
// SentinelFeignAutoConfiguration.java
@Bean
@Scope("prototype")
@ConditionalOnProperty(name = "feign.sentinel.enabled")
public Feign.Builder feignSentinelBuilder() {
    return SentinelFeign.builder();  // 返回自定义的 Builder
}

SentinelFeign.Builder.build() 内部:

// SentinelFeign.Builder (官方)
super.invocationHandlerFactory(new InvocationHandlerFactory() {
    public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
        // 获取 FallbackFactory(如果用户配置了的话)
        Object fallbackFactory = ...;
        return new SentinelInvocationHandler(target, dispatch, fallbackFactory);
    }
});
super.contract(new SentinelContractHolder(contract));

SentinelInvocationHandler.invoke() 流控逻辑:

// SentinelInvocationHandler.invoke()
ContextUtil.enter(resourceName);
Entry entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
Object result = methodHandler.invoke(args);
// catch BlockException → 若有 fallbackFactory 走 fallback;否则直接抛
// finally: entry.exit(1, args); ContextUtil.exit();

资源名由 SentinelContractHolder 收集的 MethodMetadata 构建:METHOD: + hardCodedTarget.url() + methodMetadata.template().path()

3.4.2 无感接入方式

拦截点: Feign.Builder.build() 方法入口

// FeignBuilderBuildAdvice.java
@Advice.OnMethodEnter
public static void onEnter(@Advice.This Object builder) {
    SentinelAutoInjector.beforeFeignBuild(builder);
}

注入逻辑:

// SentinelAutoInjector.beforeFeignBuild()
public static void beforeFeignBuild(Object builder) {
    // 幂等检查:已经是 SentinelFeign 则跳过
    if (builderClassName.contains("SentinelFeign")) return;

    // 1. addURL 注入 spring-cloud-starter-alibaba-sentinel JAR 到 App CL
    injectFeignAdapter(appCl);

    // 2. 用 SentinelContractHolder 包装原始 contract
    Object originalContract = contractField.get(builder);
    Object sentinelContract = contractHolderClass
        .getDeclaredConstructor(contractInterface).newInstance(originalContract);
    contractField.set(builder, sentinelContract);

    // 3. 准备双构造器:无 fallback + 有 fallback
    Constructor<?> handlerCtorNoFallback = sentinelHandlerClass
        .getDeclaredConstructor(targetClass, Map.class);
    Constructor<?> handlerCtorWithFallback = null;
    Class<?> sentinelFfClass = null;
    try {
        sentinelFfClass = Class.forName(
                "org.springframework.cloud.openfeign.FallbackFactory", true, appCl);
        handlerCtorWithFallback = sentinelHandlerClass
                .getDeclaredConstructor(targetClass, Map.class, sentinelFfClass);
    } catch (Throwable ignored) {}

    // 4. JDK 动态代理实现 InvocationHandlerFactory(支持 fallback 解析)
    Object sentinelFactory = Proxy.newProxyInstance(appCl,
        new Class[]{ invHandlerFactoryClass },
        (proxy, method, args) -> {
            if ("create".equals(method.getName())) {
                // 尝试从 @FeignClient 注解解析 fallbackFactory / fallback
                if (handlerCtorWithFallback != null) {
                    Object ff = resolveFeignFallbackFactory(
                            args[0]/*target*/, sentinelFfClass, appCl);
                    if (ff != null) {
                        return handlerCtorWithFallback.newInstance(args[0], args[1], ff);
                    }
                }
                return handlerCtorNoFallback.newInstance(args[0], args[1]);
            }
            // ...
        });
    factoryField.set(builder, sentinelFactory);
}

Fallback 解析机制:InvocationHandlerFactory.create(target, dispatch) 时自动解析:

// resolveFeignFallbackFactory() — 从 @FeignClient 注解解析降级配置
private static Object resolveFeignFallbackFactory(Object target, Class<?> sentinelFfClass, ClassLoader appCl) {
    // 1. target.type() → @FeignClient 接口(兼容 OpenFeign + Netflix 两种包路径)
    // 2. 读取 fallbackFactory / fallback 属性
    // 3. 通过无参构造器直接实例化
    // 4. 适配为 org.springframework.cloud.openfeign.FallbackFactory
    //    (feign.hystrix.FallbackFactory → JDK 动态代理适配,方法签名兼容 create(Throwable))
    // 5. 解析失败 → return null → 静默降级为无 fallback 模式
}

3.4.3 ⚠️ 与官方 Adapter 的差异

差异点 官方方式 无感接入 影响
接入方式 feign.sentinel.enabled=true(配置属性) 零配置,自动拦截所有 Feign.Builder.build() 完全透明
FallbackFactory 支持 @FeignClient(fallbackFactory = ...) 支持(自动解析注解 + 直接实例化) 简单 fallback 可正常降级
Fallback 支持 @FeignClient(fallback = ...) 支持(包装为 FallbackFactory.Default 同上
Fallback 实例化 从 Spring 容器获取(支持 @Autowired 注入) 无参构造器直接实例化(不经过 Spring 容器) 依赖 Spring 注入的复杂 fallback 字段为 null
FallbackFactory 兼容 org.springframework.cloud.openfeign.FallbackFactory 兼容两种(自动适配 feign.hystrix.FallbackFactory Netflix 1st Gen 也支持
覆盖范围 所有由 Spring 容器创建的 FeignClient 所有 Feign.Builder.build() 调用 非 Spring 管理的 Feign 也被流控
资源名 METHOD:url/path 一致,复用 SentinelContractHolder + SentinelInvocationHandler 无差异
ContextUtil.enter 一致 无差异
Tracer.traceEntry ✅ 业务异常追踪 一致 无差异

核心差异: Fallback 类通过无参构造器直接实例化,不经过 Spring 容器。对于仅使用 Logger 等静态资源的简单 fallback 类(如大部分业务 FallbackFactory 实现)可正常工作。依赖 @Autowired 注入的复杂 fallback 需业务配置 feign.sentinel.enabled=true 走官方链路。


3.5 JDK HttpURLConnection 流控

HttpURLConnection流控

3.5.1 概述

java.net.HttpURLConnection(实现类 sun.net.www.protocol.http.HttpURLConnection)是 JDK 内置 HTTP 客户端,Sentinel 官方没有提供对应 Adapter。无感接入采用直接流控模式——在 Advice 中直接反射调用 SphU.entry()/entry.exit()

3.5.2 双拦截点模型

connect()        ──── 流控主入口:创建 Entry + 阻断检查(TCP 建连之前阻断)
                      Entry 存入 ThreadLocal
                      
getInputStream() ──── 响应阶段:关闭 Entry + 异常追踪
                      从 ThreadLocal 取出 Entry 并 exit

设计原因: HttpURLConnection 的 API 模型是 connect() → getInputStream(),流控 Entry 的生命周期需要跨越两次方法调用。使用 ThreadLocal 在两个拦截点之间传递 Entry。

3.5.3 connect() 拦截

// HttpURLConnectionConnectAdvice.java
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
public static int onEnter(@Advice.This Object conn) {
    return SentinelAutoInjector.beforeUrlConnectionConnect(conn);
    // 返回 0 = 放行,1 = 被 Sentinel 阻断
}

@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void onExit(@Advice.Enter int blocked, @Advice.Thrown(readOnly = false) Throwable thrown) {
    if (blocked != 0) {
        thrown = new java.io.IOException("Blocked by Sentinel (flow limiting)");
        SentinelAutoInjector.afterUrlConnectionConnect(thrown);
    } else if (thrown != null) {
        SentinelAutoInjector.afterUrlConnectionConnect(thrown);
    }
}

流控逻辑:

// SentinelAutoInjector.beforeUrlConnectionConnect()
public static int beforeUrlConnectionConnect(Object conn) {
    // 1. 防重入:ThreadLocal 已有 Entry 则跳过
    if (CURRENT_URLCONN_ENTRY.get() != null) return 0;

    // 2. 获取 ClassLoader:HttpURLConnection 由 BootstrapCL 加载,conn.getClass().getClassLoader() == null
    //    使用 TCCL 获取 App CL,TCCL 为 null 时回退 SentinelClassBridge.getPluginClassLoader()
    ClassLoader appCl = Thread.currentThread().getContextClassLoader();
    if (appCl == null) appCl = SentinelClassBridge.getPluginClassLoader();

    // 3. 构建资源名: HttpURLConnection_{METHOD}_{path}
    String resourceName = buildUrlConnectionResourceName(method, urlStr);

    // 4. 反射调用 SphU.entry(resourceName, EntryType.OUT)
    Object entry = sentinelEntry(appCl, resourceName, 0);
    CURRENT_URLCONN_ENTRY.set(entry);
    return 0;
    // catch: 若 BlockException → return 1 (阻断)
}

3.5.4 getInputStream() 拦截

// HttpURLConnectionGetInputStreamAdvice.java
@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void onExit(@Advice.Thrown Throwable thrown) {
    SentinelAutoInjector.afterUrlConnectionGetInputStream(thrown);
}
// SentinelAutoInjector.afterUrlConnectionGetInputStream()
public static void afterUrlConnectionGetInputStream(Throwable error) {
    Object entry = CURRENT_URLCONN_ENTRY.get();
    if (entry == null) return;
    CURRENT_URLCONN_ENTRY.remove();
    if (error != null) traceException(entry, error);
    exitEntry(entry);
}

3.5.5 BootstrapCL 类加载问题

sun.net.www.protocol.http.HttpURLConnectionBootstrapClassLoader 加载。Advice 内联代码引用的 SentinelAutoInjector 必须对 BootstrapCL 可见。

ByteBuddy 方案的天然优势: SentinelAutoInjector 打包在 agent-bootstrap.jar 中,已通过 inst.appendToBootstrapClassLoaderSearch() 注入 BootstrapCL,无需额外 Spy 桥接层

独立 AgentBuilder 的必要性:AgentBuilder.Default() 默认 ignore Bootstrap 类。HttpURLConnection 使用独立 AgentBuilder 并覆盖 ignore 规则:

new AgentBuilder.Default()
    .ignore(nameStartsWith("net.bytebuddy."))  // 仅 ignore ByteBuddy 自身
    .type(named("sun.net.www.protocol.http.HttpURLConnection"))
    .transform(...)
    .installOn(inst);

3.5.6 资源名构建

private static String buildUrlConnectionResourceName(String method, String urlStr) {
    String path = new URL(urlStr).getPath();   // 提取 path 部分
    if (path == null || path.isEmpty()) path = "/";
    return "HttpURLConnection_" + method + "_" + path;
    // 示例: HttpURLConnection_POST_/api/submit
}

3.5.7 特殊场景

场景 处理方式
connect() 成功 + getInputStream() 未调用(fire-and-forget POST) Entry 留在 ThreadLocal,等待线程复用时被新 connect() 的防重入机制跳过
connect() 抛异常 onExitafterUrlConnectionConnect(thrown) 立即关闭 Entry
Sentinel 阻断 onEnter 返回 1 → skipOn 跳过原 connect()onExitIOException("Blocked by Sentinel")
TCCL 为 null 回退到 SentinelClassBridge.getPluginClassLoader()

四、MyBatis 流控

4.1 概述

Sentinel 官方不提供 MyBatis Adapter。 无感接入方案自研了 sentinel-mybatis-adapter,基于 MyBatis Interceptor 机制实现 SQL 级别流控。

4.2 Adapter 实现

4.2.1 SentinelMyBatisMapperInterceptor

// SentinelMyBatisMapperInterceptor.java
@Intercepts({
    @Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update",
        args = {MappedStatement.class, Object.class})
})
public class SentinelMyBatisMapperInterceptor implements Interceptor {

    public static final String MYBATIS_PREFIX = "mybatis:";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        String mapperId = ms.getId();   // 如: com.example.mapper.UserMapper.selectById

        Entry entry = null;
        try {
            entry = SphU.entry(MYBATIS_PREFIX + mapperId,
                ResourceTypeConstants.COMMON_DB_SQL, EntryType.OUT);
            return invocation.proceed();
        } catch (BlockException ex) {
            throw new MyBatisSentinelBlockException(ex);
        } catch (Throwable t) {
            Tracer.traceEntry(t, entry);
            throw t;
        } finally {
            if (entry != null) entry.exit();
        }
    }
}

资源名格式: mybatis:com.example.mapper.UserMapper.selectById

资源类型: ResourceTypeConstants.COMMON_DB_SQL

EntryType: OUT(作为下游资源)

4.2.2 MyBatisSentinelBlockException

public class MyBatisSentinelBlockException extends RuntimeException {
    private final BlockException blockException;

    public MyBatisSentinelBlockException(BlockException cause) {
        super(cause);
        this.blockException = cause;
    }

    public BlockException getBlockException() {
        return blockException;
    }
}

4.3 注入方式

拦截点: org.apache.ibatis.plugin.InterceptorChain.pluginAll(Object)

// MyBatisPluginAllAdvice.java
@Advice.OnMethodEnter
public static void onEnter(@Advice.This Object interceptorChain) {
    SentinelAutoInjector.beforeMyBatisPluginAll(interceptorChain);
}

注入逻辑:

// SentinelAutoInjector.beforeMyBatisPluginAll()
public static void beforeMyBatisPluginAll(Object interceptorChain) {
    // 1. addURL 注入 sentinel-mybatis-adapter JAR 到 App CL
    File adapterJar = findMyBatisAdapterJar();
    tryAddUrl(appCl, adapterJar.toURI().toURL());

    // 2. 幂等检查:检查 InterceptorChain 中是否已有 SentinelMyBatisMapperInterceptor
    if (hasInterceptor(interceptorChain, interceptorClazz.getName())) return;

    // 3. 反射创建 SentinelMyBatisMapperInterceptor 实例
    Object interceptor = interceptorClazz.getDeclaredConstructor().newInstance();

    // 4. 反射调用 InterceptorChain.addInterceptor(Interceptor) 注入
    Method addInterceptor = interceptorChain.getClass().getMethod("addInterceptor", interceptorType);
    addInterceptor.invoke(interceptorChain, interceptor);
}

时序: 在 MyBatis 初始化 Configuration.newExecutor()InterceptorChain.pluginAll(executor) 调用前注入拦截器,确保后续所有 Executor 实例都经过 Sentinel 流控包装。

4.4 流控效果

维度 说明
粒度 Mapper 方法级(mybatis: + mapperId
覆盖 Executor.query(所有 SELECT)+ Executor.update(INSERT/UPDATE/DELETE)
阻断 抛出 MyBatisSentinelBlockExceptionRuntimeException 子类,内含 BlockException
异常追踪 业务 SQL 异常通过 Tracer.traceEntry(t, entry) 记录
资源类型 COMMON_DB_SQL(在 Sentinel Dashboard 可按类型过滤)

4.5 注意事项

  • 官方无对标 Adapter:此为完全自研实现,资源名格式 mybatis:{mapperId} 无官方先例
  • 粒度较细:每个 Mapper 方法一个资源,大型项目可能产生大量资源条目
  • BlockException 包装:MyBatis 的 Interceptor.intercept() 签名为 throws Throwable,但 BlockException 是受检异常。为避免上层调用方(如 Spring 事务管理器)误处理,将其包装为 MyBatisSentinelBlockExceptionRuntimeException

五、Sentinel API 反射缓存机制

所有直接流控模式(HttpURLConnection)和 Adapter 创建逻辑均依赖反射调用 Sentinel API。为避免重复反射查找,使用 SentinelApiHolder 按 ClassLoader 缓存:

private static class SentinelApiHolder {
    final Method entryMethod;      // SphU.entry(String, EntryType)
    final Object entryTypeIn;      // EntryType.IN
    final Object entryTypeOut;     // EntryType.OUT
    final Method exitMethod;       // Entry.exit()
    final Method traceMethod;      // Tracer.trace(Throwable)

    SentinelApiHolder(ClassLoader appCl) throws Throwable {
        // 通过 appCl 加载 → ClassBridge 路由到 PluginCL
        Class<?> sphUClass = Class.forName("com.alibaba.csp.sentinel.SphU", true, appCl);
        // ...
    }
}

private static final Map<ClassLoader, SentinelApiHolder> API_CACHE =
    Collections.synchronizedMap(new WeakHashMap<>());

WeakHashMap 设计:key 为 ClassLoader,当 ClassLoader 被 GC 回收时(如 WAR 热部署),缓存条目自动清除,防止内存泄漏。


六、与官方差异汇总

6.1 注入官方 Adapter JAR 的框架

框架 注入的官方 Adapter 核心差异
Spring WebMVC sentinel-spring-webmvc-adapter SentinelWebMvcConfig,不支持 UrlCleaner/BlockExceptionHandler/httpMethodSpecify 等配置
RestTemplate spring-cloud-starter-alibaba-sentinel 覆盖所有实例(不仅 @SentinelRestTemplate 标注的),不支持 blockHandler/fallback/urlCleaner
Apache HttpClient sentinel-apache-httpclient-adapter 覆盖所有 Builder,不支持自定义 prefix/extractor/fallback
OkHttp sentinel-okhttp-adapter 覆盖所有 Builder,不支持自定义 resourcePrefix/resourceExtractor/fallback
Feign spring-cloud-starter-alibaba-sentinel 覆盖所有 Feign.Builder支持 Fallback(自动解析 @FeignClient 注解,通过无参构造器实例化;依赖 Spring 注入的复杂 fallback 需走官方链路)

6.2 通用差异模式

差异维度 原因 影响
覆盖范围更广 字节码增强拦截所有实例构造/方法调用,而非仅 Spring Bean 非业务组件(框架内部 HTTP 调用、健康检查等)也被流控
不支持自定义配置 通过反射默认构造器创建 Adapter,无法传入 Config Bean 资源名提取器、限流返回、URL 清洗等均为默认值
Fallback 部分支持 Feign 自动解析 @FeignClient 注解属性(通过无参构造器实例化);RestTemplate 的 fallback 依赖注解属性或 Spring Bean Feign 简单 fallback 可正常降级;RestTemplate 限流/熔断时使用默认 SentinelClientHttpResponse
资源名完全一致 复用官方 Adapter 的资源名生成逻辑 与官方接入的 Sentinel Dashboard 规则兼容

6.3 自研 Adapter(无官方对标)

框架 说明
MyBatis 官方无 MyBatis Adapter。自研 SentinelMyBatisMapperInterceptor,资源名 mybatis:{mapperId}
HttpURLConnection 官方无此 Adapter。直接流控模式,资源名 HttpURLConnection_{METHOD}_{path}

七、Byte Buddy Transformer 注册清单

# 目标类 拦截方法 Advice 类 模式
1 ClassLoader loadClass(String, boolean) ClassLoaderLoadClassAdvice 类桥接
2 o.a.dubbo...ExtensionLoader loadDirectory(Map, String, String) ApacheDubboLoadDirectoryAdvice Adapter JAR 注入
3 c.a.dubbo...ExtensionLoader loadDirectory(Map, String) AlibabaDubboLoadDirectoryAdvice Adapter JAR 注入
4 o.a.ibatis...InterceptorChain pluginAll(Object) MyBatisPluginAllAdvice Adapter JAR 注入
5 o.s...InterceptorRegistry getInterceptors() SpringWebMvcGetInterceptorsAdvice Adapter JAR 注入
6 o.s...RestTemplate <init>() + setInterceptors(List) RestTemplateConstructorAdvice / ...SetInterceptorsAdvice Adapter JAR 注入
7 o.a.http...HttpClientBuilder decorateMainExec(ClientExecChain) HttpClientDecorateMainExecAdvice Adapter JAR 注入
8 okhttp3.OkHttpClient$Builder build() OkHttpClientBuildAdvice Adapter JAR 注入
9 feign.Feign$Builder build() FeignBuilderBuildAdvice Adapter JAR 注入
10 sun...HttpURLConnection connect() + getInputStream() HttpURLConnectionConnectAdvice / ...GetInputStreamAdvice 直接流控
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容