• Emit学习(4)


    感觉好久没有写博客了, 这几天有点小忙, 接下来会更忙, 索性就先写一篇吧. 后面估计会有更长的一段时间不会更新博客了.

    废话不多说, 先上菜.

    一、示例

    1. 先建类, 类的名称与读取的表名并没有什么关系,可以不一样, 然后就是其中的属性大小写不限

    public class Tch_Teacher
        {
            public int Id { get; set; }
            
            public string Name { get; set; }
    
            public bool IsDoublePosition { get; set; }
    
            public DateTime CreateDate { get; set; }
        }
    
        public class Test : Tch_Teacher //, ISupportInitialize  此接口有两个方法, BeginInit, EndInit
        {
            //[ExplicitConstructor] Dapper会优先查找设置了此属性的构造函数
            //public Test() {  }  
    
            public string BId { get; set; }
    
            //public void BeginInit()
            //{
            //    Console.WriteLine("Test BeginInit");
            //}
    
            //public void EndInit()
            //{
            //    Console.WriteLine("Test EndInit");
            //}
        }

    2. 测试代码

        class Program
        {
            static void Main(string[] args)
            {
                var conStr = ConfigurationManager.ConnectionStrings["ConStr"].ToString();
                using (IDbConnection conn = new MySqlConnection(conStr))
                {
                    var sql = "select Count(1) from tch_teacher where id>@Id limit 3;";
                    //Console.WriteLine(conn.Query<int?>(sql, new { Id = 10 })); //error
                    Console.WriteLine(conn.Query<int>(sql, new { Id = 10 }).FirstOrDefault());   // 19
                    Console.WriteLine(conn.Query<string>(sql, new { Id = 1 }).FirstOrDefault()); // 19
    
                    sql = "select Id, BId, No, Name, CreateDate from tch_teacher limit 3;";  //No这个字段, 在类中并没有
                    var list = conn.Query<Test>(sql);
                    Console.WriteLine(list.ToList().FirstOrDefault().BId);  // c5f5959e-0744-42cd-a843-145e28149d9b
                }
                Console.ReadKey();
            }
        }
       

    接下来, 可以进入Dapper的部分了

    三、Dapper 开始

    Query<int/string> 和 Query<Test>在读取数据的部分是一样的, 开始出现不同的地方主要体现在 object to model 的部分,

     private static Func<IDataReader, object> GetDeserializer(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)

    首先注意一个地方
    static SqlMapper()
            {
            //这部分是 简单类型处理用到的, 当然这其中并不仅仅只有简单类型 typeMap
    = new Dictionary<Type, DbType>(); typeMap[typeof(byte)] = DbType.Byte; typeMap[typeof(sbyte)] = DbType.SByte; typeMap[typeof(short)] = DbType.Int16; typeMap[typeof(ushort)] = DbType.UInt16; typeMap[typeof(int)] = DbType.Int32; typeMap[typeof(uint)] = DbType.UInt32; typeMap[typeof(long)] = DbType.Int64; typeMap[typeof(ulong)] = DbType.UInt64; typeMap[typeof(float)] = DbType.Single; typeMap[typeof(double)] = DbType.Double; typeMap[typeof(decimal)] = DbType.Decimal; typeMap[typeof(bool)] = DbType.Boolean; typeMap[typeof(string)] = DbType.String; typeMap[typeof(char)] = DbType.StringFixedLength; typeMap[typeof(Guid)] = DbType.Guid; typeMap[typeof(DateTime)] = DbType.DateTime; typeMap[typeof(DateTimeOffset)] = DbType.DateTimeOffset; typeMap[typeof(TimeSpan)] = DbType.Time; typeMap[typeof(byte[])] = DbType.Binary; typeMap[typeof(byte?)] = DbType.Byte; typeMap[typeof(sbyte?)] = DbType.SByte; typeMap[typeof(short?)] = DbType.Int16; typeMap[typeof(ushort?)] = DbType.UInt16; typeMap[typeof(int?)] = DbType.Int32; typeMap[typeof(uint?)] = DbType.UInt32; typeMap[typeof(long?)] = DbType.Int64; typeMap[typeof(ulong?)] = DbType.UInt64; typeMap[typeof(float?)] = DbType.Single; typeMap[typeof(double?)] = DbType.Double; typeMap[typeof(decimal?)] = DbType.Decimal; typeMap[typeof(bool?)] = DbType.Boolean; typeMap[typeof(char?)] = DbType.StringFixedLength; typeMap[typeof(Guid?)] = DbType.Guid; typeMap[typeof(DateTime?)] = DbType.DateTime; typeMap[typeof(DateTimeOffset?)] = DbType.DateTimeOffset; typeMap[typeof(TimeSpan?)] = DbType.Time; typeMap[typeof(object)] = DbType.Object;         
            //这个方法可以实现自定义处理, 它是一个public static 方法             AddTypeHandlerImpl(
    typeof(DataTable), new DataTableHandler(), false); }

    然后看GetDeserializer方法

    private static Func<IDataReader, object> GetDeserializer(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
            {
    
                // dynamic is passed in as Object ... by c# design
                if (type == typeof(object)
                    || type == typeof(DapperRow))
                {
              //object / dynamic 类型, 会执行以下方法
    return GetDapperRowDeserializer(reader, startBound, length, returnNullIfFirstMissing); } Type underlyingType = null; if (!(typeMap.ContainsKey(type) || type.IsEnum || type.FullName == LinqBinary || (type.IsValueType && (underlyingType = Nullable.GetUnderlyingType(type)) != null && underlyingType.IsEnum))) { ITypeHandler handler; if (typeHandlers.TryGetValue(type, out handler)) { //自定义处理 return GetHandlerDeserializer(handler, type, startBound); } //复杂类型的处理 return GetTypeDeserializer(type, reader, startBound, length, returnNullIfFirstMissing); } //以上简单类型, 值类型, 可空值类型, 枚举, linq的二进制 的处理 return GetStructDeserializer(type, underlyingType ?? type, startBound); }

    这里我只介绍 复杂类型的处理方式了, 至于其他的, 跟Emit的主题关系不是很大, 有兴趣的童鞋, 可以自己去看一下, 应该是能看懂的

    由于 GetTypeDeserializer 这个方法实在是太长了, 我把说明都写在注释里面去吧.  按照我的注释, 应该是能看懂整个过程的. 可能还是IL那一段不太好懂, 我第一次看的时候, 就看到那里就没继续看下去了, 实在是不想继续看了. 以下是代码部分

      1 /// <summary>
      2         /// Internal use only
      3         /// </summary>
      4         /// <param name="type"></param>
      5         /// <param name="reader"></param>
      6         /// <param name="startBound"></param>
      7         /// <param name="length"></param>
      8         /// <param name="returnNullIfFirstMissing"></param>
      9         /// <returns></returns>
     10         public static Func<IDataReader, object> GetTypeDeserializer(
     11 #if CSHARP30
     12 Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing
     13 #else
     14 Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false
     15 #endif
     16 )
     17         {
     18             //创建动态方法 Deserialize[Guid]
     19             var dm = new DynamicMethod(string.Format("Deserialize{0}", Guid.NewGuid()), typeof(object), new[] { typeof(IDataReader) }, true);
     20             var il = dm.GetILGenerator();
     21             il.DeclareLocal(typeof(int));  //定义本地变量 loc0
     22             il.DeclareLocal(type);        //定义本地变量 loc1 -> target
     23             il.Emit(OpCodes.Ldc_I4_0);
     24             il.Emit(OpCodes.Stloc_0);   //初始化本地变量loc0, loc0 = 0  
     25 
     26             if (length == -1)
     27             {
     28                 length = reader.FieldCount - startBound; //获取要转换字段的个数
     29             }
     30 
     31             if (reader.FieldCount <= startBound)
     32             {
     33                 throw MultiMapException(reader);
     34             }
     35 
     36             //获取读取出来的字段名, 并转入数组中   -> string[]  Id, BId, No, Name, CreateDate
     37             var names = Enumerable.Range(startBound, length).Select(i => reader.GetName(i)).ToArray(); 
     38 
     39             ITypeMap typeMap = GetTypeMap(type);  //new DefaultTypeMap(type)
     40 
     41             int index = startBound;
     42 
     43             //有参构造函数
     44             ConstructorInfo specializedConstructor = null;
     45             //需要初始化标志
     46             bool supportInitialize = false;
     47             if (type.IsValueType)   //target是值类型
     48             {
     49                 il.Emit(OpCodes.Ldloca_S, (byte)1);  //加载loc1的地址
     50                 il.Emit(OpCodes.Initobj, type);   //初始化loc1, loc1 = 0
     51             }
     52             else   //target是引用类型
     53             {
     54                 var types = new Type[length];
     55                 for (int i = startBound; i < startBound + length; i++)
     56                 {
     57                     //获取读到的db值的类型
     58                     types[i - startBound] = reader.GetFieldType(i);
     59                 }
     60                 //查找标记了ExplicitConstructor属性(Attribute)的构造函数
     61                 var explicitConstr = typeMap.FindExplicitConstructor();
     62                 if (explicitConstr != null)
     63                 {
     64                     #region 存在
     65                     var structLocals = new Dictionary<Type, LocalBuilder>();
     66 
     67                     var consPs = explicitConstr.GetParameters(); //获取该构造函数上的参数集
     68 
     69                     #region 遍历加载参数
     70                     foreach (var p in consPs)
     71                     {
     72                         //引用类型
     73                         if (!p.ParameterType.IsValueType)
     74                         {
     75                             //如果传入参数为复杂类型, 则以 null 来处理
     76                             il.Emit(OpCodes.Ldnull);
     77                         }
     78                         else    //值类型
     79                         {
     80                             LocalBuilder loc;
     81                             if (!structLocals.TryGetValue(p.ParameterType, out loc))
     82                             {
     83                                 //定义本地变量
     84                                 structLocals[p.ParameterType] = loc = il.DeclareLocal(p.ParameterType);
     85                             }
     86 
     87                             il.Emit(OpCodes.Ldloca, (short)loc.LocalIndex);
     88                             il.Emit(OpCodes.Initobj, p.ParameterType);   //初始化传入参数, a=0,b=false之类的
     89                             il.Emit(OpCodes.Ldloca, (short)loc.LocalIndex);
     90                             il.Emit(OpCodes.Ldobj, p.ParameterType);   //加载初始化后的参数
     91                         }
     92                     }
     93                     #endregion
     94 
     95                     il.Emit(OpCodes.Newobj, explicitConstr);   //创建对象  new target(...);
     96                     il.Emit(OpCodes.Stloc_1);   //loc1 = target
     97 
     98                     //target 是否实现 ISupportInitialize 接口, 如果实现, 则调用其 BeginInit 方法
     99                     supportInitialize = typeof(ISupportInitialize).IsAssignableFrom(type);
    100                     if (supportInitialize)
    101                     {
    102                         il.Emit(OpCodes.Ldloc_1);
    103                         il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod("BeginInit"), null);
    104                     }
    105                     #endregion
    106                 }
    107                 else
    108                 {
    109                     #region 不存在
    110                     var ctor = typeMap.FindConstructor(names, types);  //查找构造函数, 优先返回无参构造函数
    111                     if (ctor == null)
    112                     {
    113                         //找不到能用的构造函数
    114                         string proposedTypes = "(" + string.Join(", ", types.Select((t, i) => t.FullName + " " + names[i]).ToArray()) + ")";
    115                         throw new InvalidOperationException(string.Format("A parameterless default constructor or one matching signature {0} is required for {1} materialization", proposedTypes, type.FullName));
    116                     }
    117 
    118                     if (ctor.GetParameters().Length == 0)
    119                     {
    120                         il.Emit(OpCodes.Newobj, ctor);
    121                         il.Emit(OpCodes.Stloc_1);   //loc1 = new target();
    122                         supportInitialize = typeof(ISupportInitialize).IsAssignableFrom(type);
    123                         if (supportInitialize)
    124                         {
    125                             il.Emit(OpCodes.Ldloc_1);
    126                             il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod("BeginInit"), null);
    127                         }
    128                     }
    129                     else
    130                     {
    131                         specializedConstructor = ctor;
    132                     }
    133                     #endregion
    134                 }
    135             }
    136 
    137             //try  开始
    138             il.BeginExceptionBlock();   
    139             if (type.IsValueType)
    140             {
    141                 //如果是值类型, 加载target的地址
    142                 il.Emit(OpCodes.Ldloca_S, (byte)1);// [target]
    143             }
    144             else if (specializedConstructor == null)   //构造函数为无参构造函数
    145             {
    146                 //引用类型, 则直接使用变量即可
    147                 il.Emit(OpCodes.Ldloc_1);// [target]
    148             }
    149 
    150             //用reader中的列去匹配target中的属性, 匹配不上, 则显示为null, 此处的No为null
    151             var members = (specializedConstructor != null
    152                 ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n))
    153                 : names.Select(n => typeMap.GetMember(n))).ToList();  //无参
    154 
    155             // stack is now [target]
    156 
    157             bool first = true;
    158             var allDone = il.DefineLabel();
    159             int enumDeclareLocal = -1,
    160                 //定义第二个本地变量,object类型的, 然后返回此本地变量的index值, 其实就是截止目前, 定义了本地变量的个数
    161                 valueCopyLocal = il.DeclareLocal(typeof(object)).LocalIndex;
    162             foreach (var item in members)
    163             {
    164                 if (item != null)
    165                 {
    166                     #region object to model
    167 
    168                     if (specializedConstructor == null)   //无参构造函数存在
    169                         il.Emit(OpCodes.Dup); // stack is now [target][target]
    170 
    171                     Label isDbNullLabel = il.DefineLabel();
    172                     Label finishLabel = il.DefineLabel();
    173 
    174                     il.Emit(OpCodes.Ldarg_0); // stack is now [target][target][reader]
    175                     EmitInt32(il, index); // stack is now [target][target][reader][index]
    176                     il.Emit(OpCodes.Dup);// stack is now [target][target][reader][index][index]
    177                     il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index]     //loc0 = [index]
    178                     //获取reader读取的值, reader[index]
    179                     il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object]  
    180                     il.Emit(OpCodes.Dup); // stack is now [target][target][value-as-object][value-as-object]
    181                     StoreLocal(il, valueCopyLocal);  //将 reader[index]的值, 存放到本地变量 loc_valueCopyLocal 中
    182 
    183                     Type colType = reader.GetFieldType(index);   //reader[index] 的列的类型  source
    184                     Type memberType = item.MemberType;   //target[item] 的类型  target
    185 
    186                     //如果目标类型为char 或者 char? , 则调用ReadChar / ReadNullableChar方法来完成转换
    187                     if (memberType == typeof(char) || memberType == typeof(char?))
    188                     {
    189                         il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(
    190                             memberType == typeof(char) ? "ReadChar" : "ReadNullableChar", BindingFlags.Static | BindingFlags.Public), null); // stack is now [target][target][typed-value]
    191                     }
    192                     else
    193                     {
    194                         il.Emit(OpCodes.Dup); // stack is now [target][target][value-as-object][value-as-object]
    195                         //判断是否为DBNull类型, 如果是, 则跳转到 标签isDbNullLabel
    196                         il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [target][target][value-as-object][DBNull or null]
    197                         il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [target][target][value-as-object]
    198 
    199                         // unbox nullable enums as the primitive, i.e. byte etc
    200                         // int? -> int,   int/string -> null, 根据可空值类型来获取其值类型
    201                         var nullUnderlyingType = Nullable.GetUnderlyingType(memberType);
    202                         var unboxType = nullUnderlyingType != null && nullUnderlyingType.IsEnum ? nullUnderlyingType : memberType;
    203 
    204                         if (unboxType.IsEnum)
    205                         {
    206                             Type numericType = Enum.GetUnderlyingType(unboxType);
    207                             if (colType == typeof(string))
    208                             {
    209                                 if (enumDeclareLocal == -1)
    210                                 {
    211                                     enumDeclareLocal = il.DeclareLocal(typeof(string)).LocalIndex;
    212                                 }
    213                                 il.Emit(OpCodes.Castclass, typeof(string)); // stack is now [target][target][string]
    214                                 StoreLocal(il, enumDeclareLocal); // stack is now [target][target]
    215                                 il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][enum-type-token]
    216                                 il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null);// stack is now [target][target][enum-type]
    217                                 LoadLocal(il, enumDeclareLocal); // stack is now [target][target][enum-type][string]
    218                                 il.Emit(OpCodes.Ldc_I4_1); // stack is now [target][target][enum-type][string][true]
    219                                 il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [target][target][enum-as-object]
    220                                 il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value]
    221                             }
    222                             else
    223                             {
    224                                 FlexibleConvertBoxedFromHeadOfStack(il, colType, unboxType, numericType);
    225                             }
    226 
    227                             if (nullUnderlyingType != null)
    228                             {
    229                                 il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })); // stack is now [target][target][typed-value]
    230                             }
    231                         }
    232                         else if (memberType.FullName == LinqBinary)
    233                         {
    234                             il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [target][target][byte-array]
    235                             il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) }));// stack is now [target][target][binary]
    236                         }
    237                         else
    238                         {
    239                             TypeCode dataTypeCode = Type.GetTypeCode(colType), 
    240                                 unboxTypeCode = Type.GetTypeCode(unboxType);
    241                             bool hasTypeHandler;
    242                             if ((hasTypeHandler = typeHandlers.ContainsKey(unboxType)) || colType == unboxType || dataTypeCode == unboxTypeCode || dataTypeCode == Type.GetTypeCode(nullUnderlyingType))
    243                             {
    244                                 //判断是否有自定义的转换方法, 如果有, 则调用自定义的方法完成转换
    245                                 if (hasTypeHandler)
    246                                 {
    247 #pragma warning disable 618
    248                                     il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod("Parse"), null); // stack is now [target][target][typed-value]
    249 #pragma warning restore 618
    250                                 }
    251                                 else
    252                                 {
    253                                     //将指令中指定类型的已装箱的表示形式转换成未装箱形式
    254                                     il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value]
    255                                 }
    256                             }
    257                             else
    258                             {
    259                                 // not a direct match; need to tweak the unbox
    260                                 FlexibleConvertBoxedFromHeadOfStack(il, colType, nullUnderlyingType ?? unboxType, null);
    261                                 if (nullUnderlyingType != null)
    262                                 {
    263                                     il.Emit(OpCodes.Newobj, unboxType.GetConstructor(new[] { nullUnderlyingType })); // stack is now [target][target][typed-value]
    264                                 }
    265                             }
    266                         }
    267                     }
    268                     if (specializedConstructor == null)
    269                     {
    270                         // Store the value in the property/field
    271                         if (item.Property != null)
    272                         {
    273                             if (type.IsValueType)
    274                             {
    275                                 il.Emit(OpCodes.Call, DefaultTypeMap.GetPropertySetter(item.Property, type)); // stack is now [target]
    276                             }
    277                             else
    278                             {
    279                                 il.Emit(OpCodes.Callvirt, DefaultTypeMap.GetPropertySetter(item.Property, type)); // stack is now [target]
    280                             }
    281                         }
    282                         else
    283                         {
    284                             il.Emit(OpCodes.Stfld, item.Field); // stack is now [target]
    285                         }
    286                     }
    287 
    288                     il.Emit(OpCodes.Br_S, finishLabel); // stack is now [target]
    289 
    290                     il.MarkLabel(isDbNullLabel); // incoming stack: [target][target][value]
    291                     if (specializedConstructor != null)
    292                     {
    293                         il.Emit(OpCodes.Pop);
    294                         if (item.MemberType.IsValueType)
    295                         {
    296                             int localIndex = il.DeclareLocal(item.MemberType).LocalIndex;
    297                             LoadLocalAddress(il, localIndex);
    298                             il.Emit(OpCodes.Initobj, item.MemberType);
    299                             LoadLocal(il, localIndex);
    300                         }
    301                         else
    302                         {
    303                             il.Emit(OpCodes.Ldnull);
    304                         }
    305                     }
    306                     else
    307                     {
    308                         il.Emit(OpCodes.Pop); // stack is now [target][target]
    309                         il.Emit(OpCodes.Pop); // stack is now [target]
    310                     }
    311 
    312                     if (first && returnNullIfFirstMissing)
    313                     {
    314                         il.Emit(OpCodes.Pop);
    315                         il.Emit(OpCodes.Ldnull); // stack is now [null]
    316                         il.Emit(OpCodes.Stloc_1);
    317                         il.Emit(OpCodes.Br, allDone);
    318                     }
    319 
    320                     il.MarkLabel(finishLabel);
    321                     #endregion
    322                 }
    323 
    324                 first = false;
    325                 index += 1;
    326             }
    327             if (type.IsValueType)
    328             {
    329                 il.Emit(OpCodes.Pop);
    330             }
    331             else
    332             {
    333                 //构造函数为有参的构造函数
    334                 if (specializedConstructor != null)
    335                 {
    336                     //创建对象
    337                     il.Emit(OpCodes.Newobj, specializedConstructor);
    338                 }
    339                 il.Emit(OpCodes.Stloc_1); // stack is empty
    340 
    341                 //实现 ISupportInitialize 接口, 调用 EndInit 方法, 完成初始化
    342                 if (supportInitialize)
    343                 {
    344                     il.Emit(OpCodes.Ldloc_1);
    345                     il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod("EndInit"), null);
    346                 }
    347             }
    348             il.MarkLabel(allDone);
    349             //try 结束 -> catch 开始
    350             il.BeginCatchBlock(typeof(Exception)); // stack is Exception
    351             il.Emit(OpCodes.Ldloc_0); // stack is Exception, index
    352             il.Emit(OpCodes.Ldarg_0); // stack is Exception, index, reader
    353             LoadLocal(il, valueCopyLocal); // stack is Exception, index, reader, value
    354             il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("ThrowDataException"), null);  //抛出异常
    355             il.EndExceptionBlock();
    356             //catch 结束
    357 
    358             il.Emit(OpCodes.Ldloc_1); // stack is [rval]   此处就是转换后的最终结果
    359             if (type.IsValueType)
    360             {
    361                 il.Emit(OpCodes.Box, type);
    362             }
    363             il.Emit(OpCodes.Ret);
    364 
    365             return (Func<IDataReader, object>)dm.CreateDelegate(typeof(Func<IDataReader, object>));
    366         }

    其中的value-as-object是从reader中读取出来的未转换的数据, typed-value是转换后的数据

    本想做成可收缩的, 但是那种展开后, 不能点, 只要一点, 就自动收起来了, 感觉不方便, 所以还是贴出来了

    其中还有些地方不够详细, 不过对照着这个, 去看Dapper源码, 是可以看的懂了

     在下一篇中, 我会画出堆栈中的变化, 来减少理解难度

  • 相关阅读:
    HUE配置文件hue.ini 的impala模块详解(图文详解)(分HA集群)
    poj 1815 Friendship (最小割+拆点+枚举)
    Android项目中包名的改动
    Android MediaPlayer Error -1004
    hive原生和复合类型的数据载入和使用
    Linux学习笔记之权限与命令之间的关系(重要)及文件与文件夹知识总结
    kafka集群搭建与apiclient创建
    Android中各种Adapter的使用方法
    【从零学习Python】Ubuntu14.10下Python开发环境配置
    leetcode
  • 原文地址:https://www.cnblogs.com/elvinle/p/6043701.html
Copyright © 2020-2023  润新知