Tars | 第8篇 TarsJava Subset最终代码的执行流程与原理分析
前言
中期汇报会后,对Tars Subset功能更加熟悉,并根据TarsGo的实现方式,对Java JDK实现代码进行翻新改造。于是有了以下两篇分析文章:
第5篇 基于TarsGo Subset路由规则的Java JDK实现方式(上篇)
https://www.cnblogs.com/dlhjw/p/15245113.html
第6篇 基于TarsGo Subset路由规则的Java JDK实现方式(下篇)
https://www.cnblogs.com/dlhjw/p/15245116.html
其中,《上篇》注重TarsGo分析,《下篇》注重TarsJava实现方式。不出意外的话,最终提交的考核成果就在下面的GitHub代码仓库中(以下简称“最终代码”),后续可能会有些许地方需要更改:
TarsJava 实现Subset路由规则JDK GitHub开源地址
https://github.com/dlhjw/TarsJava/commit/cc2fe884ecbe8455a8e1f141e21341f4f3dd98a3
最终代码与中期代码在整体思想逻辑上都是一致的都是:先判断Subset路由规则,再根据规则路由到定义的节点。不同点在于:中期在处理整个过程时,用一个方法filterEndpointsBySubset()实现;而最终的实现方式则是以subsetEndpointFilter()方法作为整个Subset流量路由的入口,通过subsetManager管理器调用getSubset()方法获取到路由规则的String类型的subset字段,与节点自身的subset字段一一比较过滤节点;其中Subset路由规则的判断封装在getSubset()方法里;
总的来说就是最终代码是在处理subset规则逻辑中增加了很多细节,比如:通过新增的registry接口获取subsetConf配置项;将获取到是配置项存入缓存中;以及将“判断Subset路由规则”进行层层封装,最终返回一个简单的String类型的subset字段与节点自身的subset字段比较等;
因此,在执行流程上相比较中期有些许区别,其中比较复杂的地方涉及对各种封装方法的调用与传参。本篇将结合最终代码的实现逻辑,以debug的方式,重点介绍其执行流程,即:
- 首先找到过滤节点的方法入口;
 - 接着通过管理者尝试获取String类型的规则
subset字段; - 通过
getSubset()方法匹配路由规则; - 调用具体路由规则的
findSubet()方法获取最终的规则subset字段; - 将规则
subset字段与节点的subset字段对比,实现筛选功能; 
通过下面分析可以得出一个Subset的业务执行流程结构图,如下:
- subset.subsetEndpointFilter():整个业务流程的方法入口;
- subsetManager.getSubset():通过XxxConf配置获取String类型的subset字段;
- getSubsetConfig():从缓存或registry接口获取SubsetConf;
 - subsetConf.getRatioConf():获取RatioConf配置;
- ratioConf.findSubet():通过RatioConf配置获取String类型的subset字段;
 
 - subsetConf.getKeyConf():获取KeyConf配置;
- keyConf.findSubet():通过KeyConf配置获取String类型的subset字段;
 
 
 - for循环匹配subset字段;
 
 - subsetManager.getSubset():通过XxxConf配置获取String类型的subset字段;
 
当然,涉及到通过新增的registry接口获取subsetConf配置项等逻辑,这些细节就放在下面正文里讲吧;
最终代码的Subset测试方案设计请参考下面这篇文章:
第7篇 TarsJava Subset最终代码的测试方案设计
https://www.cnblogs.com/dlhjw/p/15245121.html
《测试方案设计》与《执行流程分析》两篇文章相辅相成,相互观阅能更快更好地理解整个Subset的业务流程与输出示例;
1. SubsetConf配置项的结构
在中期,笔者使用一个map来模拟subset的流量规则;而在最终代码里,是用多个对象来模拟Subset的配置,这些对象是理解整个Subset流量过滤规则的基础的,因此很有必要在这里做个介绍;
1.1 SubsetConf
public class SubsetConf {
    private boolean enanle;
    private String ruleType;
    private RatioConfig ratioConf;
    private KeyConfig keyConf;
    private Instant lastUpdate;
    ……
}
可以看出SubsetConf配置项里有以下属性:
- enanle:表示是否开启Subset流量管理功能;
- true:开启;false:关闭;
 
 - ruleType:表示流量管理的类型;
- 目前有
ratio按比例和key按参数两种模式; 
 - 目前有
 - RatioConfig:表示按比例路由配置项;
- 里面定义了路由比例与路径等信息,详情请参考《1.2 RatioConfig》
 
 - KeyConfig:表示按参数路由配置项;
- 里面定义了规则key与路由路径等信息,详情请参考《1.3 KeyConfig》
 
 - lastUpdate:表示该配置项上次更新时间,将在缓存那里起作用;
 
1.2 RatioConfig
public class RatioConfig {
    private Map<String, Integer> rules;
    ……
}
RatioConfig里只有一个map类型的rules路由规则,其中key为一个String类型的subset字段,用来跟节点的subset字段匹配,value为路由权重,如:{ {"v1" , 20} , {"v2" , 60} , {"v3" , 20} }表示路由到subset字段为v1的节点的概率为0.2;路由到subset字段为v2的节点的概率为0.6;路由到subset字段为v3的节点的概率为0.2;
1.3 KeyConfig
public class KeyConfig {
    private String defaultRoute;
    private List<KeyRoute> rules;
    ……
}
KeyConfig里有两个属性,一个是defaultRoute默认路由路径;另一个是list类型的rules,里面是KeyRoute,其定义了按参数匹配的类型、规则key与路径,详情请见《1.4 KeyRoute》
1.4 KeyRoute
public class KeyRoute {
    private String action = null;
    private String value = null;
    private String route = null;
    public static final String TARS_ROUTE_KEY = "TARS_ROUTE_KEY";
    ……
}
KeyRoute里面有四个String类型的属性,如下:
- action:用来定义参数匹配的类型;
- 目前可设置的类型有:equals精确匹配、match正则匹配、default默认匹配;
 
 - value:这就是大名鼎鼎的规则key了。当action=equals时,还需满足规则key与请求key匹配,才能进行精确匹配;当action=match时,还需满足规则key与请求key正则匹配,才能进行正则匹配;action=default对规则key没要求;
 - route:用来规定路由路径,其值为一个String类型的subset字段,匹配到节点的subset字段;
 - TARS_ROUTE_KEY:一个常量字段,为Tars请求体里的status(map类型)的key;
 
1.5 SubsetConf的结构示意图
上述提到的配置类联系结构图如下:

从SubsetConf配置类可以看出,按比例路由和按参数在思路上有些许不同,因此下面dubug分析将分为两种情况;
2. 过滤节点的方法入口
新增的Sunset流量路由规则应该在查找服务端节点那里,其目的是对获取到的服务端节点根据subset规则进行过滤,因此整个Subset业务逻辑的入口函数在getServerNodes()方法的subsetEndpointFilter()里,如下图所示:

我们给该方法打上断点,进行debug调试;
3. subsetEndpointFilter()方法解析
3.1 方法功能
| 方法名 | subsetEndpointFilter() | 
|---|---|
| 方法所在类 | Subset.java | 
| 方法逻辑 | 根据Subset路由规则过滤节点 | 
| 传入参数 | 服务名、透传染色key、存活节点列表 | 
| 传出参数 | 过滤后的存活节点列表 | 
3.2 方法源码
public Holder<List<EndpointF>> subsetEndpointFilter(String servantName, String routeKey, Holder<List<EndpointF>> eps){
    if( subsetConf==null || !subsetConf.isEnanle() ){
        return eps;
    }
    if(eps.value == null || eps.value.isEmpty()){
        return eps;
    }
    //调用subsetManager,根据比例/匹配等规则获取到路由规则的subset
    String subset = subsetManager.getSubset(servantName, routeKey);
    if( "".equals(subset) || subset == null){
        return eps;
    }
    //和每一个eps的subset比较,淘汰不符合要求的
    Holder<List<EndpointF>> epsFilter = new Holder<>(new ArrayList<EndpointF>());
    for (EndpointF ep : eps.value) {
        if( subset.equals(ep.getSubset())){
            epsFilter.getValue().add(ep);
        }
    }
    return epsFilter;
}
3.3 方法解析
进入subsetEndpointFilter()方法,首先会对subsetConf配置项进行非空校验,这里的配置项是从前端传过来的,正常情况下不为空;接着对传入的参数存活节点列表eps做非空校验。两步校验通过后,进行核心方法getSubset()的调用,返回String类型的规则的subset字段,该字段可以表示路由路径。然后将规则的subset字段与节点的subset字段一一比较,选出符合要求的节点;
测试结果详情请见《TarsJava Subset最终代码的测试方案设计》一文;
返回的String类型的规则的subset字段在下图中就是RotioConfig的rules中的value或者KeyConfig的rules中的route,表示最终路由路径。以下所有方法都是围绕如何找到这个规则的subset字段做文章;

我们进入getSubset()方法查看Tars是怎么获取到规则的subset字段的;
4. getSubset()方法解析
4.1 方法功能
| 方法名 | getSubset() | 
|---|---|
| 方法所在类 | SubsetManager.java | 
| 方法逻辑 | 根据路由规则先获取到比例 / 染色路由的配置,再通过配置获取String类型的subset字段 | 
| 传入参数 | 服务名、透传染色key | 
| 传出参数 | String类型的规则的subset字段 | 
4.2 方法源码
public String getSubset(String servantName, String routeKey){
    //check subset config exists
    SubsetConf subsetConf = getSubsetConfig(servantName);
    if( subsetConf == null ){
        return null;
    }
    // route key to subset
    if("ratio".equals(subsetConf.getRuleType())){
        RatioConfig ratioConf = subsetConf.getRatioConf();
        if(ratioConf != null){
            return ratioConf.findSubet(routeKey);
        }
    }
    KeyConfig keyConf = subsetConf.getKeyConf();
    if ( keyConf != null ){
        return keyConf.findSubet(routeKey);
    }
    return null;
}
4.3 方法解析
该方法首先调用getSubsetConfig()方法获取到一个SubsetConf配置项,该配置项可以是从缓存中拿,也可以是通过registry接口获取(详情请见《5. getSubsetConfig()方法解析》);
确保SubsetConf配置项存在后,识别SubsetConf配置项的ruleType路由类型属性(按比例、按参数、默认),通过属性获取到对应的配置项(ratioConf、keyConf、keyConf),最后调用findSubet()方法即可获取到String类型的规则的subset字段,并返回(详情请见《6. 按比例路由的findSubet()方法解析》与《7. 按参数路由的findSubet()方法解析》);
我们先进入getSubsetConfig()方法探其源码;
5. getSubsetConfig()方法解析
5.1 方法功能
| 方法名 | |
|---|---|
| 方法所在类 | SubsetManager.java | 
| 方法逻辑 | 获取SubsetConf路由规则配置项,并存到subsetConf配置项 | 
| 传入参数 | 服务名 | 
| 传出参数 | SubsetConf配置项 | 
5.2 方法源码
public SubsetConf getSubsetConfig(String servantName){
    SubsetConf subsetConf = new SubsetConf();
    if( cache.containsKey(servantName) ){
        subsetConf = cache.get(servantName);
        //小于10秒从缓存中取
        if( Duration.between(subsetConf.getLastUpdate() , Instant.now()).toMillis() < 1000 ){
            return subsetConf;
        }
    }
    // get config from registry
    Holder<SubsetConf> subsetConfHolder = new Holder<SubsetConf>(subsetConf);
    int ret = queryProxy.findSubsetConfigById(servantName, subsetConfHolder);
    SubsetConf newSubsetConf = subsetConfHolder.getValue();
    if( ret == TarsHelper.SERVERSUCCESS ){
        return newSubsetConf;
    }
    //从registry中获取失败时,更新subsetConf添加进缓存
    subsetConf.setRuleType( newSubsetConf.getRuleType() );
    subsetConf.setLastUpdate( Instant.now() );
    cache.put(servantName, subsetConf);
    //解析subsetConf
    if( !newSubsetConf.isEnanle() ){
        subsetConf.setEnanle(false);
        return subsetConf;
    }
    if( "ratio".equals(newSubsetConf.getRuleType())){
        subsetConf.setRatioConf( newSubsetConf.getRatioConf() );
    } else {
        //按参数匹配
        KeyConfig newKeyConf = newSubsetConf.getKeyConf();
        List<KeyRoute> keyRoutes = newKeyConf.getRules();
        for ( KeyRoute kr: keyRoutes) {
            KeyConfig keyConf = new KeyConfig();
            //默认
            if("default".equals(kr.getAction())){
                keyConf.setDefaultRoute(newKeyConf.getDefaultRoute());
                subsetConf.setKeyConf(keyConf);
            }
            //精确匹配
            if("match".equals(kr.getAction())){
                List<KeyRoute> rule = new ArrayList<>();
                rule.add(new KeyRoute("match", kr.getValue() , kr.getRoute()));
                keyConf.setRules( rule );
            }
            //正则匹配
            if("equal".equals(kr.getAction())){
                List<KeyRoute> rule = new ArrayList<>();
                rule.add(new KeyRoute("equal", kr.getValue() , kr.getRoute()));
                keyConf.setRules( rule );
            }
        }
        subsetConf.setKeyConf(newKeyConf);
    }
    return subsetConf;
}
5.3 方法解析
getSubsetConfig()方法的获取主要分为两个逻辑:从缓存中获取与从registry接口获取,判断依据是:如果缓存中的subsetConf配置项的lastUpdate上次更新属性落后当前时间小于10秒,则直接从缓存中获取;否则重新调用registry接口获取subsetConf配置项;
测试结果详情请见《TarsJava Subset最终代码的测试方案设计》一文;
这是与另一个新增功能registry接口接触的方法,由于registry功能还未实现,这部分功能做不了测试,但其大概逻辑是通过registry接口查阅数据库获取到subsetConf配置项;
getSubsetConfig()方法执行完后,回到4. getSubset()方法里,我们再进入findSubet()方法窥其究竟;
6. 按比例路由的findSubet()方法解析
需要注意,按比例、按参数路由的findSubet()方法实现方式有些许不同,故在这里分开来讲;
6.1 方法功能
| 方法名 | |
|---|---|
| 方法所在类 | RatioConfig.java | 
| 方法逻辑 | 根据权重比例获取rules中最后的subset字段 | 
| 传入参数 | 透传的染色key | 
| 传出参数 | String类型的规则的subset字段 | 
6.2 方法源码
public String findSubet(String routeKey){
     //routeKey为空时随机
     if( "".equals(routeKey) || routeKey == null){
         //赋值routeKey为获取的随机值
         Random random = new Random();
         int r = random.nextInt( rules.size() );
         routeKey = String.valueOf(r);
         int i = 0;
         for (String key : rules.keySet()) {
             if(i == r){
                 return key;
             }
             i++;
         }
     }
     //routeKey不为空时实现按比例算法
     int totalWeight = 0;
     int supWeight = 0;
     String subset = null;
     //获得总权重
     for (Integer value : rules.values()) {
         totalWeight+=value;
     }
     //获取随机数
     Random random = new Random();
     int r = random.nextInt(totalWeight);
     //根据随机数找到subset
     for (Map.Entry<String, Integer> entry : rules.entrySet()){
         supWeight+=entry.getValue();
         if( r < supWeight){
             subset = entry.getKey();
             return subset;
         }
     }
     return null;
 }
6.3 方法解析
首先判断透传的染色key是否为空,如果为空,则会进行等比例随机路由到任意一个value;如果不为空,则会计算权重,通过随机数匹配权重的方式按比例路由到对应的value,该value就是需要返回给入口函数的String类型的规则的subset字段;
该方法实现下图的蓝圈1号流程:

而在按参数路由里,也有一个findSubet()方法,他们的传入传出参数相同,实现功能类似,但在逻辑上有些不同,下面来看一下吧;
7. 按参数路由的findSubet()方法解析
7.1 方法功能
| 方法名 | |
|---|---|
| 方法所在类 | KeyConfig.java | 
| 方法逻辑 | 根据参数匹配规则获取rules中最后的subset字段 | 
| 传入参数 | 透传的染色key | 
| 传出参数 | String类型的规则的subset字段 | 
7.2 方法源码
public String findSubet(String routeKey){
    //非空校验
    if( routeKey == null || "".equals(routeKey) || rules == null){
        return null;
    }
    for ( KeyRoute rule: rules) {
        //根据根据分布式上下文信息获取 “请求的染色的key”
        String routeKeyReq;
        if( distributedContext != null){
            routeKeyReq = KeyRoute.getRouteKey(distributedContext);
        } else {
            logger.info("无分布式上下文信息distributedContext");
            return null;
        }
        //精确匹配
        if( "match".equals(rule.getAction())  ){
            if( routeKeyReq.equals(rule.getValue()) ){
                return rule.getRoute();
            } else {
                logger.info("染色key匹配不上,请求的染色key为:" + routeKeyReq + "; 规则的染色key为:" + rule.getValue());
            }
        }
        //正则匹配
        if( "equal".equals(rule.getAction()) ){
            if( StringUtils.matches(routeKeyReq, rule.getValue()) ){
                return rule.getRoute();
            } else {
                logger.info("正则匹配失败,请求的染色key为:" + routeKeyReq + "; 规则的染色key为:" + rule.getValue());
            }
        }
        //默认匹配
        if( "default".equals(rule.getAction()) ){
            //默认路由无需考虑染色key
            return rule.getRoute();
        }
    }
    return null;
}
7.3 方法解析
按参数匹配路由方式要求跟Taes请求体的染色key做匹配比较,因此这部分逻辑需要加上;
首先进行非空校验,校验通过后遍历KeyRoute,只需要找到符合条件的rules即可处理返回;在for循环里,首先根据分布式上下文信息获取 “请求的染色key”,根据KeyRoute的action分情况讨论:
- action=match:精确匹配,需要判断请求的染色key和规则的染色key是否相等,相等才能进行精确路由;不相等则不进行路由,并输出一句错误日志;
 - action=equal:正则匹配,其中正则式为请求的染色key,需要与规则的染色key进行匹配,同理匹配成功才能进行精确路由;否则则不进行路由,并输出一句错误日志;
 - action=default:默认路由,不需要使用染色key,路由到用户设定的默认路由路径;
 - 测试结果详情请见《TarsJava Subset最终代码的测试方案设计》一文;
 
该方法实现下图的蓝圈2号流程:

通过上述步骤,最终获取到的规则的subset字段将返回到入口函数进行后续步骤:即将规则的subset字段与节点的subset字段一一比较,选出符合要求的节点;
8 总结:Subset业务执行流程结构图
通过上述分析,可以知道整个Subset的业务执行流程结构图如下:
- subset.subsetEndpointFilter():整个业务流程的方法入口;
- subsetManager.getSubset():通过XxxConf配置获取String类型的subset字段;
- getSubsetConfig():从缓存或registry接口获取SubsetConf;
 - subsetConf.getRatioConf():获取RatioConf配置;
- ratioConf.findSubet():通过RatioConf配置获取String类型的subset字段;
 
 - subsetConf.getKeyConf():获取KeyConf配置;
- keyConf.findSubet():通过KeyConf配置获取String类型的subset字段;
 
 
 - for循环匹配subset字段;
 
 - subsetManager.getSubset():通过XxxConf配置获取String类型的subset字段;
 
最后
新人制作,如有错误,欢迎指出,感激不尽!
欢迎关注公众号,会分享一些更日常的东西!
如需转载,请标注出处!

Tars | 第8篇 TarsJava Subset最终代码的执行流程与原理分析的更多相关文章
- Tars | 第7篇 TarsJava Subset最终代码的测试方案设计
		
目录 前言 1. SubsetConf配置项的结构 1.1 SubsetConf 1.2 RatioConfig 1.3 KeyConfig 1.4 KeyRoute 1.5 SubsetConf的结 ...
 - Tars | 第2篇 TarsJava SpingBoot启动与负载均衡源码初探
		
目录 前言 1. Tars客户端启动 @EnableTarsServer 2. Communicator通信器 3. 客户端的负载均衡调用器LoadBalance 最后 前言 通过源码分析可以得出这样 ...
 - 面经手册 · 第17篇《码农会锁,ReentrantLock之AQS原理分析和实践使用》
		
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 如果你相信你做什么都能成,你会自信的多! 千万不要总自我否定,尤其是职场的打工人.如 ...
 - mirantis  fuel puppet执行顺序  和 对整个项目代码的执行流程理解
		
stage执行顺序 stage {'zero': } -> stage {'first': } -> stage {'openstack-custom-repo': } -> sta ...
 - Tars | 第0篇 腾讯犀牛鸟开源人才培养计划Tars实战笔记目录
		
腾讯犀牛鸟开源人才培养计划Tars实战笔记目录 前言 在2021年夏,笔者参加了腾讯首届开源人才培养计划的Tars项目,负责Subset流量管理规则的Java语言JDK实现.其中写作几篇开源实战笔记, ...
 - Java中异常发生时代码执行流程
		
异常与错误: 异常: 在Java中程序的错误主要是语法错误和语义错误,一个程序在编译和运行时出现的错误我们统一称之为异常,它是VM(虚拟机)通知你的一种方式,通过这种方式,VM让你知道,你(开发人员) ...
 - Tars | 第5篇 基于TarsGo Subset路由规则的Java JDK实现方式(上)
		
目录 前言 1. 修改.tars协议文件 1.1 Go语言修改部分 1.2 修改地方的逻辑 1.3 通过协议文件自动生成代码 2. [核心]增添Subset核心功能 2.1 Go语言修改部分 2.2 ...
 - .Net判断一个对象是否为数值类型探讨总结(高营养含量,含最终代码及跑分)
		
前一篇发出来后引发了积极的探讨,起到了抛砖引玉效果,感谢大家参与. 吐槽一下:这个问题比其看起来要难得多得多啊. 大家的讨论最终还是没有一个完全正确的答案,不过我根据讨论结果总结了一个差不多算是最终版 ...
 - 意外作出了一个javascript的服务器,可以通过js调用并执行任何java(包括 所有java 内核基本库)及C#类库,并最终由 C# 执行你提交的javascript代码! 不敢藏私,特与大家分
		
最近研发BDC 云开发部署平台的数据路由及服务管理器意外作出了一个javascript的服务器,可以通过js调用并执行任何java(包括 所有java 内核基本库)及C#类库,并最终由 C# 执行你提 ...
 
随机推荐
- C++ //函数调用运算符重载 (仿函数)
			
1 //函数调用运算符重载 2 3 #include <iostream> 4 #include <string> 5 using namespace std; 6 7 //函 ...
 - Visio操作【未完】
			
Visio 1.如何操作文档 新建基本框图和空白框图 单击基本框图打开后有模具 空白框图打开之后并没有形状 左下角发现有 更改纸张方向大小 自动调整大小: 如果我们选择形状进入到我们的页面,如果放到边 ...
 - 实战爬取拷背漫画-Python
			
 一.抓包获取链接 以爬取<前科者>为例 获取搜索链接 https://api.copymanga.com/api/v3/search/comic?limit=5&q=前科者 ...
 - 关于XSS简单介绍与waf bypass的一些思路整理
			
很久没写东西了,今天整理一点儿思路 简单说一下XSS XSS(cross site script)即跨站脚本,侧重于"脚本"这一层概念,是一种常见web安全漏洞.攻击者通过往web ...
 - SwiftUI图片处理(缩放、拼图)
			
采用SwiftUI Core Graphics技术,与C#的GDI+绘图类似,具体概念不多说,毕竟我也是新手,本文主要展示效果图及代码,本文示例代码需要请拉到文末自取. 1.图片缩放 完全填充,变形压 ...
 - WPF下获取文件运行路径、运行文件名等
			
在客户端开发过程中,经常需要获取相对路径的一些资源,而相对路径的就与客户端运行文件的路径息息相关了.在以前的winform开发中,我们可以使用 System.Windows.Forms.Applica ...
 - 接口测试--测试工具:rap2 接口文档解析
			
通过百度 OCR 工具识别 rap2 登录中的验证码,从而实现登录~那我们今天来实战解析 rap2 的接口数据,生成我们所需要的接口数据 实践上手 文档分析 1.我们先通过 F12 看看哪个接口是我们 ...
 - Socket入门Demo
			
一.简单介绍下Socket编程 申明:.net网络编程 1)什么是Socket编程? Socket编程就是常说的网络通讯编程,套接字编程.一般应用于软件聊天通讯,以及软件与硬件之间的通讯. 通熟 ...
 - 【springcloud】一文带你搞懂API网关
			
作者:aCoder2013 https://github.com/aCoder2013/blog/issues/35 前言 假设你正在开发一个电商网站,那么这里会涉及到很多后端的微服务,比如会员.商品 ...
 - spring 》Cglib赋值
			
第一个:字节码文件时带有ENHANCERBYCGLIB,FastClassByCGLIB组成的文件名 第二个:字节码文件时带有ENHANCERBYCGLIB 第三个:字节码文件时带有FastClass ...