找回密码
 立即注册
首页 业界区 业界 Sentinel源码—7.参数限流和注解的实现

Sentinel源码—7.参数限流和注解的实现

后仲舒 2025-6-2 21:58:12
大纲
1.参数限流的原理和源码
2.@SentinelResource注解的使用和实现
 
1.参数限流的原理和源码
(1)参数限流规则ParamFlowRule的配置Demo
(2)ParamFlowSlot根据参数限流规则验证请求
 
(1)参数限流规则ParamFlowRule的配置Demo
一.参数限流的应用场景
二.参数限流规则的属性
三.参数限流规则的配置Demo
 
一.参数限流的应用场景
传统的流量控制,一般是通过资源维度来限制某接口或方法的调用频率。但有时需要更细粒度地控制不同参数条件下的访问速率,即参数限流。参数限流允许根据不同的参数条件设置不同的流量控制规则,这种方式非常适合处理特定条件下的请求,因为能更加精细地管理流量。
 
假设有一个在线电影订票系统,某个接口允许用户查询电影的放映时间。但只希望每个用户每10秒只能查询接口1次,以避免过多的查询请求。这时如果直接将接口的QPS限制为5是不能满足要求的,因为需求是每个用户每5分钟只能查询1次,而不是每秒一共只能查询5次,因此参数限流就能派上用场了。
 
可以设置一个规则,根据用户ID来限制每个用户的查询频率,将限流的维度从资源维度细化到参数维度,从而实现每个用户每10秒只能查询接口1次。比如希望影院工作人员可以每秒查询10次,老板可以每秒查询100次,而购票者则只能每10秒查询一次,其中工作人员的userId值为100和200,老板的userId值为9999,那么可以如下配置:需要注意限流阈值是以秒为单位的,所以需要乘以统计窗口时长10。
1.webp
二.参数限流规则的属性
  1. public class ParamFlowRule extends AbstractRule {
  2.     ...
  3.     //The threshold type of flow control (0: thread count, 1: QPS).
  4.     //流量控制的阈值类型(0表示线程数,1表示QPS)
  5.     private int grade = RuleConstant.FLOW_GRADE_QPS;
  6.     //Parameter index.
  7.     //参数下标
  8.     private Integer paramIdx;
  9.     //The threshold count.
  10.     //阈值
  11.     private double count;
  12.     //Original exclusion items of parameters.
  13.     //针对特定参数的流量控制规则列表
  14.     private List<ParamFlowItem> paramFlowItemList = new ArrayList<ParamFlowItem>();
  15.    
  16.     //Indicating whether the rule is for cluster mode.
  17.     //是否集群
  18.     private boolean clusterMode = false;
  19.     ...
  20. }
  21. //针对特定参数的流量控制规则
  22. public class ParamFlowItem {
  23.     private String object;
  24.     private Integer count;
  25.     private String classType;
  26.     ...
  27. }
复制代码
三.参数限流规则的配置Demo
  1. //This demo demonstrates flow control by frequent ("hot spot") parameters.
  2. public class ParamFlowQpsDemo {
  3.     private static final int PARAM_A = 1;
  4.     private static final int PARAM_B = 2;
  5.     private static final int PARAM_C = 3;
  6.     private static final int PARAM_D = 4;
  7.     //Here we prepare different parameters to validate flow control by parameters.
  8.     private static final Integer[] PARAMS = new Integer[] {PARAM_A, PARAM_B, PARAM_C, PARAM_D};
  9.     private static final String RESOURCE_KEY = "resA";
  10.    
  11.     public static void main(String[] args) throws Exception {
  12.         initParamFlowRules();
  13.         final int threadCount = 20;
  14.         ParamFlowQpsRunner<Integer> runner = new ParamFlowQpsRunner<>(PARAMS, RESOURCE_KEY, threadCount, 120);
  15.         runner.tick();
  16.         Thread.sleep(1000);
  17.         runner.simulateTraffic();
  18.     }
  19.     private static void initParamFlowRules() {
  20.         //QPS mode, threshold is 5 for every frequent "hot spot" parameter in index 0 (the first arg).
  21.         ParamFlowRule rule = new ParamFlowRule(RESOURCE_KEY)
  22.             .setParamIdx(0)
  23.             .setGrade(RuleConstant.FLOW_GRADE_QPS)
  24.             .setCount(5);
  25.         //We can set threshold count for specific parameter value individually.
  26.         //Here we add an exception item. That means:
  27.         //QPS threshold of entries with parameter `PARAM_B` (type: int) in index 0 will be 10, rather than the global threshold (5).
  28.         ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
  29.             .setClassType(int.class.getName())
  30.             .setCount(10);
  31.         rule.setParamFlowItemList(Collections.singletonList(item));
  32.         //ParamFlowRuleManager类加载的一个时机是:它的静态方法被调用了
  33.         //所以下面会先初始化ParamFlowRuleManager,再执行loadRules()方法
  34.         ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
  35.     }
  36. }
  37. public final class ParamFlowRuleManager {
  38.     private static final Map<String, List<ParamFlowRule>> PARAM_FLOW_RULES = new ConcurrentHashMap<>();
  39.     private final static RulePropertyListener PROPERTY_LISTENER = new RulePropertyListener();
  40.     private static SentinelProperty<List<ParamFlowRule>> currentProperty = new DynamicSentinelProperty<>();
  41.     static {
  42.         currentProperty.addListener(PROPERTY_LISTENER);
  43.     }
  44.     //Load parameter flow rules. Former rules will be replaced.
  45.     public static void loadRules(List<ParamFlowRule> rules) {
  46.         try {
  47.             //设置规则的值为rules
  48.             currentProperty.updateValue(rules);
  49.         } catch (Throwable e) {
  50.             RecordLog.info("[ParamFlowRuleManager] Failed to load rules", e);
  51.         }
  52.     }
  53.     static class RulePropertyListener implements PropertyListener<List<ParamFlowRule>> {
  54.         @Override
  55.         public void configUpdate(List<ParamFlowRule> list) {
  56.             Map<String, List<ParamFlowRule>> rules = aggregateAndPrepareParamRules(list);
  57.             if (rules != null) {
  58.                 PARAM_FLOW_RULES.clear();
  59.                 PARAM_FLOW_RULES.putAll(rules);
  60.             }
  61.             RecordLog.info("[ParamFlowRuleManager] Parameter flow rules received: {}", PARAM_FLOW_RULES);
  62.         }
  63.         @Override
  64.         public void configLoad(List<ParamFlowRule> list) {
  65.             Map<String, List<ParamFlowRule>> rules = aggregateAndPrepareParamRules(list);
  66.             if (rules != null) {
  67.                 PARAM_FLOW_RULES.clear();
  68.                 PARAM_FLOW_RULES.putAll(rules);
  69.             }
  70.             RecordLog.info("[ParamFlowRuleManager] Parameter flow rules received: {}", PARAM_FLOW_RULES);
  71.         }
  72.         ...
  73.     }
  74.     ...
  75. }
  76. public class DynamicSentinelProperty<T> implements SentinelProperty<T> {
  77.     protected Set<PropertyListener<T>> listeners = new CopyOnWriteArraySet<>();
  78.     private T value = null;
  79.     public DynamicSentinelProperty() {
  80.     }
  81.     //添加监听器到集合
  82.     @Override
  83.     public void addListener(PropertyListener<T> listener) {
  84.         listeners.add(listener);
  85.         //回调监听器的configLoad()方法初始化规则配置
  86.         listener.configLoad(value);
  87.     }
  88.     //更新值
  89.     @Override
  90.     public boolean updateValue(T newValue) {
  91.         //如果值没变化,直接返回
  92.         if (isEqual(value, newValue)) {
  93.             return false;
  94.         }
  95.         RecordLog.info("[DynamicSentinelProperty] Config will be updated to: {}", newValue);
  96.         //如果值发生了变化,则遍历监听器,回调监听器的configUpdate()方法更新对应的值
  97.         value = newValue;
  98.         for (PropertyListener<T> listener : listeners) {
  99.             listener.configUpdate(newValue);
  100.         }
  101.         return true;
  102.     }
  103.     ...
  104. }
  105. //A traffic runner to simulate flow for different parameters.
  106. class ParamFlowQpsRunner<T> {
  107.     private final T[] params;
  108.     private final String resourceName;
  109.     private int seconds;
  110.     private final int threadCount;
  111.     private final Map<T, AtomicLong> passCountMap = new ConcurrentHashMap<>();
  112.     private final Map<T, AtomicLong> blockCountMap = new ConcurrentHashMap<>();
  113.     private volatile boolean stop = false;
  114.     public ParamFlowQpsRunner(T[] params, String resourceName, int threadCount, int seconds) {
  115.         this.params = params;
  116.         this.resourceName = resourceName;
  117.         this.seconds = seconds;
  118.         this.threadCount = threadCount;
  119.         for (T param : params) {
  120.             passCountMap.putIfAbsent(param, new AtomicLong());
  121.             blockCountMap.putIfAbsent(param, new AtomicLong());
  122.         }
  123.     }
  124.     public void tick() {
  125.         Thread timer = new Thread(new TimerTask());
  126.         timer.setName("sentinel-timer-task");
  127.         timer.start();
  128.     }
  129.     public void simulateTraffic() {
  130.         for (int i = 0; i < threadCount; i++) {
  131.             Thread t = new Thread(new RunTask());
  132.             t.setName("sentinel-simulate-traffic-task-" + i);
  133.             t.start();
  134.         }
  135.     }
  136.     final class TimerTask implements Runnable {
  137.         @Override
  138.         public void run() {
  139.             long start = System.currentTimeMillis();
  140.             System.out.println("Begin to run! Go go go!");
  141.             System.out.println("See corresponding metrics.log for accurate statistic data");
  142.             Map<T, Long> map = new HashMap<>(params.length);
  143.             for (T param : params) {
  144.                 map.putIfAbsent(param, 0L);
  145.             }
  146.             while (!stop) {
  147.                 sleep(1000);
  148.                 //There may be a mismatch for time window of internal sliding window.
  149.                 //See corresponding `metrics.log` for accurate statistic log.
  150.                 for (T param : params) {
  151.                     System.out.println(String.format(
  152.                         "[%d][%d] Parameter flow metrics for resource %s: pass count for param <%s> is %d, block count: %d",
  153.                         seconds, TimeUtil.currentTimeMillis(), resourceName, param,
  154.                         passCountMap.get(param).getAndSet(0), blockCountMap.get(param).getAndSet(0)
  155.                     ));
  156.                 }
  157.                 System.out.println("=============================");
  158.                 if (seconds-- <= 0) {
  159.                     stop = true;
  160.                 }
  161.             }
  162.             long cost = System.currentTimeMillis() - start;
  163.             System.out.println("Time cost: " + cost + " ms");
  164.             System.exit(0);
  165.         }
  166.     }
  167.     final class RunTask implements Runnable {
  168.         @Override
  169.         public void run() {
  170.             while (!stop) {
  171.                 Entry entry = null;
  172.                 T param = generateParam();
  173.                 try {
  174.                     entry = SphU.entry(resourceName, EntryType.IN, 1, param);
  175.                     //Add pass for parameter.
  176.                     passFor(param);
  177.                 } catch (BlockException e) {
  178.                     //block.incrementAndGet();
  179.                     blockFor(param);
  180.                 } catch (Exception ex) {
  181.                     //biz exception
  182.                     ex.printStackTrace();
  183.                 } finally {
  184.                     //total.incrementAndGet();
  185.                     if (entry != null) {
  186.                         entry.exit(1, param);
  187.                     }
  188.                 }
  189.                 sleep(ThreadLocalRandom.current().nextInt(0, 10));
  190.             }
  191.         }
  192.     }
  193.     //Pick one of provided parameters randomly.
  194.     private T generateParam() {
  195.         int i = ThreadLocalRandom.current().nextInt(0, params.length);
  196.         return params[i];
  197.     }
  198.     private void passFor(T param) {
  199.         passCountMap.get(param).incrementAndGet();
  200.     }
  201.     private void blockFor(T param) {
  202.         blockCountMap.get(param).incrementAndGet();
  203.     }
  204.     private void sleep(int timeMs) {
  205.         try {
  206.             TimeUnit.MILLISECONDS.sleep(timeMs);
  207.         } catch (InterruptedException e) {
  208.         }
  209.     }
  210. }
复制代码
四.参数限流是如何进行数据统计
由于参数限流的数据统计需要细化到参数值的维度,所以使用参数限流时需要注意OOM问题。比如根据用户ID进行限流,且用户数量有几千万,那么CacheMap将会包含几千万个不会被移除的键值对,而且会随着进程运行时间的增长而不断增加,最后可能会导致OOM。
  1. @Spi(order = -3000)
  2. public class ParamFlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
  3.     @Override
  4.     public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
  5.         boolean prioritized, Object... args) throws Throwable {
  6.         //1.如果没配置参数限流规则,直接触发下一个Slot
  7.         if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
  8.             fireEntry(context, resourceWrapper, node, count, prioritized, args);
  9.             return;
  10.         }
  11.         //2.如果配置了参数限流规则,则调用ParamFlowSlot的checkFlow()方法,该方法执行完成后再触发下一个Slot
  12.         checkFlow(resourceWrapper, count, args);
  13.         fireEntry(context, resourceWrapper, node, count, prioritized, args);
  14.     }
  15.     @Override
  16.     public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
  17.         fireExit(context, resourceWrapper, count, args);
  18.     }
  19.     void applyRealParamIdx(/*@NonNull*/ ParamFlowRule rule, int length) {
  20.         int paramIdx = rule.getParamIdx();
  21.         if (paramIdx < 0) {
  22.             if (-paramIdx <= length) {
  23.                 rule.setParamIdx(length + paramIdx);
  24.             } else {
  25.                 //Illegal index, give it a illegal positive value, latter rule checking will pass.
  26.                 rule.setParamIdx(-paramIdx);
  27.             }
  28.         }
  29.     }
  30.     void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
  31.         //1.如果没传递参数,则直接放行,代表不做参数限流逻辑
  32.         if (args == null) {
  33.             return;
  34.         }
  35.         //2.如果没给resourceWrapper这个资源配置参数限流规则,则直接放行
  36.         if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
  37.             return;
  38.         }
  39.         //3.获取此资源的全部参数限流规则,规则可能会有很多个,所以是个List
  40.         List<ParamFlowRule> rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName());
  41.         //4.遍历获取到的参数限流规则
  42.         for (ParamFlowRule rule : rules) {
  43.             //进行参数验证
  44.             applyRealParamIdx(rule, args.length);
  45.             //Initialize the parameter metrics.
  46.             ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);
  47.             //进行验证的核心方法:检查当前规则是否允许通过此请求,如果不允许,则抛出ParamFlowException异常
  48.             if (!ParamFlowChecker.passCheck(resourceWrapper, rule, count, args)) {
  49.                 String triggeredParam = "";
  50.                 if (args.length > rule.getParamIdx()) {
  51.                     Object value = args[rule.getParamIdx()];
  52.                     //Assign actual value with the result of paramFlowKey method
  53.                     if (value instanceof ParamFlowArgument) {
  54.                         value = ((ParamFlowArgument) value).paramFlowKey();
  55.                     }
  56.                     triggeredParam = String.valueOf(value);
  57.                 }
  58.                 throw new ParamFlowException(resourceWrapper.getName(), triggeredParam, rule);
  59.             }
  60.         }
  61.     }
  62. }
复制代码
五.参数限流验证请求的流程图总结
2.webp
 
2.@SentinelResource注解的使用和实现
(1)@SentinelResource注解的使用
(2)@SentinelResource注解和实现
 
(1)@SentinelResource注解的使用
一.引入Sentinel Spring Boot Starter依赖
  1. //Rule checker for parameter flow control.
  2. public final class ParamFlowChecker {
  3.     public static boolean passCheck(ResourceWrapper resourceWrapper, /*@Valid*/ ParamFlowRule rule, /*@Valid*/ int count, Object... args) {
  4.         if (args == null) {
  5.             return true;
  6.         }
  7.         //1.判断参数索引是否合法,这个就是配置参数限流时设置的下标,从0开始,也就是对应args里的下标
  8.         //比如0就代表args数组里的第一个参数,如果参数不合法直接放行,相当于参数限流没生效  
  9.         int paramIdx = rule.getParamIdx();
  10.         if (args.length <= paramIdx) {
  11.             return true;
  12.         }
  13.         //2.判断参数值是不是空,如果是空直接放行
  14.         //Get parameter value.
  15.         Object value = args[paramIdx];
  16.         //Assign value with the result of paramFlowKey method
  17.         if (value instanceof ParamFlowArgument) {
  18.             value = ((ParamFlowArgument) value).paramFlowKey();
  19.         }
  20.         //If value is null, then pass
  21.         if (value == null) {
  22.             return true;
  23.         }
  24.         //3.集群限流
  25.         if (rule.isClusterMode() && rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
  26.             return passClusterCheck(resourceWrapper, rule, count, value);
  27.         }
  28.         //4.单机限流
  29.         return passLocalCheck(resourceWrapper, rule, count, value);
  30.     }
  31.     private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count, Object value) {
  32.         try {
  33.             if (Collection.class.isAssignableFrom(value.getClass())) {//基本类型
  34.                 for (Object param : ((Collection)value)) {
  35.                     if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
  36.                         return false;
  37.                     }
  38.                 }
  39.             } else if (value.getClass().isArray()) {//数组类型
  40.                 int length = Array.getLength(value);
  41.                 for (int i = 0; i < length; i++) {
  42.                     Object param = Array.get(value, i);
  43.                     if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
  44.                         return false;
  45.                     }
  46.                 }
  47.             } else {//其他类型,也就是引用类型
  48.                 return passSingleValueCheck(resourceWrapper, rule, count, value);
  49.             }
  50.         } catch (Throwable e) {
  51.             RecordLog.warn("[ParamFlowChecker] Unexpected error", e);
  52.         }
  53.         return true;
  54.     }
  55.     static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount, Object value) {
  56.         if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {//类型是QPS
  57.             if (rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) {
  58.                 //流控效果为排队等待
  59.                 return passThrottleLocalCheck(resourceWrapper, rule, acquireCount, value);
  60.             } else {
  61.                 //流控效果为直接拒绝
  62.                 return passDefaultLocalCheck(resourceWrapper, rule, acquireCount, value);
  63.             }
  64.         } else if (rule.getGrade() == RuleConstant.FLOW_GRADE_THREAD) {//类型是Thread
  65.             Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
  66.             long threadCount = getParameterMetric(resourceWrapper).getThreadCount(rule.getParamIdx(), value);
  67.             if (exclusionItems.contains(value)) {
  68.                 int itemThreshold = rule.getParsedHotItems().get(value);
  69.                 return ++threadCount <= itemThreshold;
  70.             }
  71.             long threshold = (long)rule.getCount();
  72.             return ++threadCount <= threshold;
  73.         }
  74.         return true;
  75.     }
  76.     ...
  77. }
复制代码
二.为方法添加@SentinelResource注解
下面的代码为sayHello()方法添加了@SentinelResource注解,并指定了资源名称为sayHello以及熔断降级时的回调方法fallback()。这样在请求sayHello()方法后,就可以在Sentinel Dashboard上看到此资源,然后就可以针对此资源进行一系列的规则配置了。
  1. //Rule checker for parameter flow control.
  2. public final class ParamFlowChecker {
  3.     ...
  4.     static boolean passThrottleLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount, Object value) {
  5.         ParameterMetric metric = getParameterMetric(resourceWrapper);
  6.         CacheMap<Object, AtomicLong> timeRecorderMap = metric == null ? null : metric.getRuleTimeCounter(rule);
  7.         if (timeRecorderMap == null) {
  8.             return true;
  9.         }
  10.         //Calculate max token count (threshold)
  11.         Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
  12.         long tokenCount = (long)rule.getCount();
  13.         if (exclusionItems.contains(value)) {
  14.             tokenCount = rule.getParsedHotItems().get(value);
  15.         }
  16.         if (tokenCount == 0) {
  17.             return false;
  18.         }
  19.         long costTime = Math.round(1.0 * 1000 * acquireCount * rule.getDurationInSec() / tokenCount);
  20.         while (true) {
  21.             long currentTime = TimeUtil.currentTimeMillis();
  22.             AtomicLong timeRecorder = timeRecorderMap.putIfAbsent(value, new AtomicLong(currentTime));
  23.             if (timeRecorder == null) {
  24.                 return true;
  25.             }
  26.             //AtomicLong timeRecorder = timeRecorderMap.get(value);
  27.             long lastPassTime = timeRecorder.get();
  28.             long expectedTime = lastPassTime + costTime;
  29.             if (expectedTime <= currentTime || expectedTime - currentTime < rule.getMaxQueueingTimeMs()) {
  30.                 AtomicLong lastPastTimeRef = timeRecorderMap.get(value);
  31.                 if (lastPastTimeRef.compareAndSet(lastPassTime, currentTime)) {
  32.                     long waitTime = expectedTime - currentTime;
  33.                     if (waitTime > 0) {
  34.                         lastPastTimeRef.set(expectedTime);
  35.                         try {
  36.                             TimeUnit.MILLISECONDS.sleep(waitTime);
  37.                         } catch (InterruptedException e) {
  38.                             RecordLog.warn("passThrottleLocalCheck: wait interrupted", e);
  39.                         }
  40.                     }
  41.                     return true;
  42.                 } else {
  43.                     Thread.yield();
  44.                 }
  45.             } else {
  46.                 return false;
  47.             }
  48.         }
  49.     }
  50.     private static ParameterMetric getParameterMetric(ResourceWrapper resourceWrapper) {
  51.         //Should not be null.
  52.         return ParameterMetricStorage.getParamMetric(resourceWrapper);
  53.     }
  54. }
复制代码
(2)@SentinelResource注解和实现
利用Spring AOP拦截@SentinelResource注解,最后调用SphU.entry()方法来进行处理。
[code]//Aspect for methods with {@link SentinelResource} annotation.@Aspectpublic class SentinelResourceAspect extends AbstractSentinelAspectSupport {    //SentinelResource注解    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")    public void sentinelResourceAnnotationPointcut() {    }    @Around("sentinelResourceAnnotationPointcut()")    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {        //获取方法        Method originMethod = resolveMethod(pjp);        //获取方法上的SentinelResource注解,有了这个注解,就可以获取到注解的各种属性值了        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);        if (annotation == null) {            //Should not go through here.            throw new IllegalStateException("Wrong state for SentinelResource annotation");        }        //获取资源名称        String resourceName = getResourceName(annotation.value(), originMethod);        //获取资源类型        EntryType entryType = annotation.entryType();        int resourceType = annotation.resourceType();        //创建一个Entry对象,通过SphU.entry(resourceName)将当前方法纳入Sentinel的保护体系        //如果当前资源的调用未触发任何Sentinel规则,则正常执行被拦截的方法,否则将执行对应的限流、熔断降级等处理逻辑        Entry entry = null;        try {            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());            return pjp.proceed();        } catch (BlockException ex) {            //发生异常时,通过反射执行在注解中设置的降级方法            return handleBlockException(pjp, annotation, ex);        } catch (Throwable ex) {            Class

相关推荐

您需要登录后才可以回帖 登录 | 立即注册