C# 使用Emit实现动态AOP框架 进阶篇之优化
目 录
在前几篇文章中,有几个遗留问题还没有处理:
1、切面特性对象的InterceptType属性没有处理,分别取值OnEntry(只触发入口)、OnExit(只触发出口)、All(都触发);
2、代理类中,各代理方法中的相关特性调用代码冗余,随着特性的增多,这些代码也成倍增加;
LogAttribute logAttribute = new LogAttribute();
logAttribute.OnEntry(aspectContext);
base.Age = value;
logAttribute.OnExit(aspectContext);
3、AspectExceptionAttribute我们只是修改了 ILGenerateProxyMethod,实现了异常处理,但是造成的结果时所有方法和属性(Get、Set方法)都进行了异常处理;
4、一般属性切入时,我们只切入Set方法,而现在Get、Set都进行了切入,灵活性不够;
下面我们针对这四个问题进行优化:
增加公共的切面特性调用方法( _OnEntryExit)
方法如下:
protected override void _OnEntryExit(string text, AspectContext context, AspectAttribute[] array)
{
for (int i = ; i < array.Length; i++)
{
InterceptType interceptType = array[i].InterceptType;
if (text.Equals("OnEntry") && interceptType.HasFlag(InterceptType.OnEntry))
{
array[i].OnEntry(context);
}
if (text.Equals("OnExit") && interceptType.HasFlag(InterceptType.OnExit))
{
array[i].OnExit(context);
}
}
}
方法调用代码如下:
public override int NYearLater(int num)
{
AspectContext aspectContext = new AspectContext(this, "NYearLater", new object[]
{
num
});
AspectAttribute[] array = new AspectAttribute[]
{
(AspectAttribute)new LogAttribute(InterceptType.All),
(AspectAttribute)new PerformanceAttribute(InterceptType.All)
};
int num2;
try
{
this._OnEntryExit("OnEntry", aspectContext, array);
num2 = base.NYearLater(num);
aspectContext.Result = num2;
this._OnEntryExit("OnExit", aspectContext, array);
}
catch (Exception ex)
{
aspectContext.Result = ex;
this._OnEntryExit("OnEntry", aspectContext, new AspectAttribute[]
{
(AspectAttribute)new AspectExceptionAttribute(InterceptType.All)
});
}
finally
{
this._OnEntryExit("OnExit", aspectContext, new AspectAttribute[]
{
(AspectAttribute)new AspectExceptionAttribute(InterceptType.All)
});
}
return num2;
}
上边标红的代码中看以看到,除去异常处理外的所有特性先存储到一个基于其父类的AspectAttribute[]数组中,调用通用方法_OnEntryExit管理特性切入的触发,并在_OnEntryExit中处理拦截类型,有效的解决了问题1、2,下边我们看一下生成AspectAttribute[]数组和_OnEntryExit方法的Emit代码:
生成AspectAttribute[]数组:
private static LocalBuilder CreateAspectAttributeArray(ILGenerator il_ProxyMethod, object[] aspectAttributes)
{
int aspectCount = aspectAttributes.Length; LocalBuilder array = il_ProxyMethod.DeclareLocal(typeof(AspectAttribute[]));
//数组长度入栈
il_ProxyMethod.Emit(OpCodes.Ldc_I4, aspectCount);
//生成新数组
il_ProxyMethod.Emit(OpCodes.Newarr, typeof(AspectAttribute));
//赋值给局部数组变量
il_ProxyMethod.Emit(OpCodes.Stloc, array); //遍历参数,并存入数组
for (int i = ; i < aspectCount; i++)
{
il_ProxyMethod.Emit(OpCodes.Ldloc, array);//数组入栈
il_ProxyMethod.Emit(OpCodes.Ldc_I4, i);//数组下标入栈 var aspectType = aspectAttributes[i].GetType(); ConstructorInfo constructor = aspectType.GetConstructor(new Type[] { typeof(InterceptType) }); //获取当前特性拦截类型
il_ProxyMethod.Emit(OpCodes.Ldc_I4, (int)(aspectAttributes[i] as AspectAttribute).InterceptType); //生成新的特性对象
il_ProxyMethod.Emit(OpCodes.Newobj, constructor);
il_ProxyMethod.Emit(OpCodes.Castclass, typeof(AspectAttribute));
//存入数组
il_ProxyMethod.Emit(OpCodes.Stelem_Ref);
}
return array;
}
_OnEntryExit方法:
private static MethodBuilder CreateOnEntryExit(TypeBuilder typeBuilder)
{
var result = typeBuilder.DefineMethod("_OnEntryExit", MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual,
typeof(void), new Type[] { typeof(string), typeof(AspectContext), typeof(AspectAttribute[]) }); ILGenerator gen = result.GetILGenerator();
//临时变量i
LocalBuilder locI = gen.DeclareLocal(typeof(int));
Label lbCondition = gen.DefineLabel();
Label lbTrue = gen.DefineLabel(); //if 标签
Label lbIfEntryRet = gen.DefineLabel(); Label lbIfExitRet = gen.DefineLabel(); //i=0
gen.Emit(OpCodes.Ldc_I4_0);
gen.Emit(OpCodes.Stloc, locI); //跳至判断
gen.Emit(OpCodes.Br, lbCondition); //标记True代码
gen.MarkLabel(lbTrue); //声明拦截类型变量,存储当前切面特性的拦截类型 InterceptType interceptType = array[i].InterceptType;
LocalBuilder interceptType = gen.DeclareLocal(typeof(InterceptType)); gen.Emit(OpCodes.Ldarg_3);
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldelem_Ref);
gen.Emit(OpCodes.Callvirt, typeof(AspectAttribute).GetMethod("get_InterceptType"));
gen.Emit(OpCodes.Box, typeof(InterceptType));
gen.Emit(OpCodes.Stloc, interceptType); //if 条件 if (text.Equals("OnEntry")
gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Ldstr, "OnEntry");
gen.Emit(OpCodes.Call, typeof(String).GetMethod("Equals", new Type[] { typeof(string) }));
gen.Emit(OpCodes.Brfalse, lbIfEntryRet); //if 条件 && interceptType.HasFlag(InterceptType.OnEntry))
gen.Emit(OpCodes.Ldloc, interceptType);
gen.Emit(OpCodes.Ldc_I4, );
gen.Emit(OpCodes.Box, typeof(InterceptType));
gen.Emit(OpCodes.Call, typeof(Enum).GetMethod("HasFlag"));
gen.Emit(OpCodes.Brfalse, lbIfEntryRet); //True
gen.Emit(OpCodes.Ldarg_3);
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldelem_Ref);
gen.Emit(OpCodes.Ldarg_2);
gen.Emit(OpCodes.Callvirt, typeof(AspectAttribute).GetMethod("OnEntry")); gen.MarkLabel(lbIfEntryRet); //if 条件 if (text.Equals("OnExit"))
gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Ldstr, "OnExit");
gen.Emit(OpCodes.Call, typeof(String).GetMethod("Equals", new Type[] { typeof(string) }));
gen.Emit(OpCodes.Brfalse, lbIfExitRet); //if 条件 (&& interceptType.HasFlag(InterceptType.OnExit))
gen.Emit(OpCodes.Ldloc, interceptType);
gen.Emit(OpCodes.Ldc_I4, );
gen.Emit(OpCodes.Box, typeof(InterceptType));
gen.Emit(OpCodes.Call, typeof(Enum).GetMethod("HasFlag"));
gen.Emit(OpCodes.Brfalse, lbIfExitRet); //True
gen.Emit(OpCodes.Ldarg_3);
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldelem_Ref);
gen.Emit(OpCodes.Ldarg_2);
gen.Emit(OpCodes.Callvirt, typeof(AspectAttribute).GetMethod("OnExit")); gen.MarkLabel(lbIfExitRet); //追加代码 //i++
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldc_I4_1);
gen.Emit(OpCodes.Add);
gen.Emit(OpCodes.Stloc, locI); //判断代码
gen.MarkLabel(lbCondition);
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldarg_3);
gen.Emit(OpCodes.Ldlen);
gen.Emit(OpCodes.Conv_I4); gen.Emit(OpCodes.Clt);
//如果True,跳至true代码
gen.Emit(OpCodes.Brtrue, lbTrue); //********
gen.Emit(OpCodes.Nop); gen.Emit(OpCodes.Ret); return result;
}
#endregion
这个方法稍微复杂了一些,有一个循环和两个判断,比直接写C#要难于理解。
增加两个特性,并修改ILGenerateProxyMethod方法解决问题3、4
作用于类的切面范围特性,如下:
/// <summary>
/// 切面范围特性
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class AspectRangeAttribute : Attribute
{
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
public AspectRangeAttribute()
{
Type = XAOP.AspectRangeType.Method | XAOP.AspectRangeType.Setter;
} /// <summary>
/// 构造函数
/// </summary>
/// <param name="aspectType">切面类型</param>
public AspectRangeAttribute(AspectRangeType aspectType)
{
Type = aspectType;
}
#endregion public AspectRangeType Type { get; set; } } /// <summary>
/// 切面类型
/// </summary>
public enum AspectRangeType
{
#region 枚举值 /// <summary>
/// 构造函数
/// </summary>
Constructor = , /// <summary>
/// 方法
/// </summary>
Method = , /// <summary>
/// 属性的Set方法
/// </summary>
Setter = , /// <summary>
/// 属性的Get方法
/// </summary>
Getter = , /// <summary>
/// 所有
/// </summary>
All = #endregion
}
作用于方法和属性的不切入特性:
/// <summary>
/// 不切入特性
/// </summary>
[AttributeUsage(AttributeTargets.Method| AttributeTargets.Property)]
public class NoAspectAttribute : Attribute
{ }
如果切面范围特性的取值是AspectRangeType.Method | AspectRangeType.Setter;那么我们看一个属性对应的代码
public override string Name
{
get
{
return base.Name;
}
set
{
AspectContext aspectContext = new AspectContext(this, "set_Name", new object[]
{
value
});
AspectAttribute[] array = new AspectAttribute[]
{
(AspectAttribute)new LogAttribute(InterceptType.OnExit)
};
this._OnEntryExit("OnEntry", aspectContext, array);
base.Name = value;
this._OnEntryExit("OnExit", aspectContext, array);
}
}
我们可以看到Get方法只是重写了一下,而Set方法进行了切入,这样我们就需要一个重写方法和一个切入后的代理方法,而ILGenerateProxyMethod只实现切入方法,下边我们将去除ILGenerateProxyMethod方法,将其拆分为两个类,一个类(ILOverrideMethod)实现重写方法,一个类(ILProxyMethod)实现切入后的代理方法(它继承ILOverrideMethod),而一个方法调用大概是三个阶段,执行方法体、获取返回值、返回;下边我们看两个类的实现:
ILOverrideMethod:
/// <summary>
/// 重写方法
/// </summary>
public class ILOverrideMethod
{
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
/// <param name="srcMethod">源方法</param>
/// <param name="overrideIL">override方法IL</param>
public ILOverrideMethod(MethodInfo srcMethod, ILGenerator overrideIL)
{
SrcMethod = srcMethod; OverrideMethodIL = overrideIL; ParamTypes = XDynamic.GetMethodParameterTypes(SrcMethod);
}
#endregion #region 属性 /// <summary>
/// 源方法
/// </summary>
public MethodInfo SrcMethod { get; set; } /// <summary>
/// 重写方法IL
/// </summary>
public ILGenerator OverrideMethodIL { get; set; } #endregion #region Protected /// <summary>
/// 返回值
/// </summary>
protected LocalBuilder Result; /// <summary>
/// 方法参数列表
/// </summary>
protected Type[] ParamTypes; #endregion #region 生成重写方法 Create
/// <summary>
/// 生成重写方法
/// </summary>
public virtual void Create()
{
CreateMethodBody();
57
58 GetResult();
59
60 Return();
}
#endregion #region 创建方法体 CreateMethodBody
/// <summary>
/// 创建方法体
/// </summary>
protected virtual void CreateMethodBody()
{
//类对象,参数值依次入栈并调用基类的方法
for (int i = ; i <= ParamTypes.Length; i++)
OverrideMethodIL.Emit(OpCodes.Ldarg, i); OverrideMethodIL.Emit(OpCodes.Call, SrcMethod);
}
#endregion #region 获取结果 GetResult
/// <summary>
/// 获取结果
/// </summary>
protected virtual void GetResult()
{
//如果有返回值,保存返回值到局部变量
if (SrcMethod.ReturnType != typeof(void))
{
Result = OverrideMethodIL.DeclareLocal(SrcMethod.ReturnType);
OverrideMethodIL.Emit(OpCodes.Stloc, Result);
}
}
#endregion #region 返回 Return
/// <summary>
/// 返回
/// </summary>
protected virtual void Return()
{
//如果有返回值,则把返回值压栈
if (Result != null)
OverrideMethodIL.Emit(OpCodes.Ldloc, Result); OverrideMethodIL.Emit(OpCodes.Ret);//返回
}
#endregion
}
ILProxyMethod:
/// <summary>
/// 代理方法
/// </summary>
public class ILProxyMethod : ILOverrideMethod
{
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
/// <param name="srcMethod">源方法</param>
/// <param name="il"></param>
/// <param name="aspectAttributes">切面特性集合</param>
/// <param name="onEntryExit">切面入口出口统一调用方法</param>
public ILProxyMethod(MethodInfo srcMethod, ILGenerator il, object[] aspectAttributes, MethodInfo onEntryExit)
: base(srcMethod, il)
{
OnEntryExit = onEntryExit; AspectExceptionAttributes = aspectAttributes.Where(p => p.GetType() == typeof(AspectExceptionAttribute)).ToArray(); HandleException = AspectExceptionAttributes.Length > ; //生成切面上下文
AspectContext = CreateAspectContext(OverrideMethodIL, SrcMethod.Name, ParamTypes);
//生成除去异常特性外的特性数组
AspectAttributes = CreateAspectAttributeArray(OverrideMethodIL,
aspectAttributes.Where(p => p.GetType() != typeof(AspectExceptionAttribute)).ToArray());
}
#endregion #region Protected /// <summary>
/// 切面入口出口统一调用方法
/// </summary>
protected MethodInfo OnEntryExit { get; set; } /// <summary>
/// 切面上下文
/// </summary>
protected LocalBuilder AspectContext; /// <summary>
/// 除去异常外的切面特性数组
/// </summary>
protected LocalBuilder AspectAttributes { get; set; } /// <summary>
/// 异常特性
/// </summary>
protected object[] AspectExceptionAttributes; /// <summary>
/// 是否处理异常
/// </summary>
protected bool HandleException = false; #endregion #region 创建方法体 CreateMethodBody
/// <summary>
/// 创建方法体
/// </summary>
protected override void CreateMethodBody()
{
if (HandleException)
OverrideMethodIL.BeginExceptionBlock(); //调用横切对象的OnEntryt方法
CallOn_Entry_Exit(OverrideMethodIL, true, AspectContext, AspectAttributes, OnEntryExit); base.CreateMethodBody();
}
#endregion #region 获取结果 GetResult
/// <summary>
/// 获取结果
/// </summary>
protected override void GetResult()
{
base.GetResult(); if (SrcMethod.ReturnType != typeof(void))
{
//给AspectContext的属性Result赋值
var resultSetMethod = typeof(AspectContext).GetMethod("set_Result");
OverrideMethodIL.Emit(OpCodes.Ldloc, AspectContext); //加载AspectContext局部变量
OverrideMethodIL.Emit(OpCodes.Ldloc, Result);//加载返回值
OverrideMethodIL.Emit(OpCodes.Box, SrcMethod.ReturnType);
OverrideMethodIL.Emit(OpCodes.Call, resultSetMethod);//赋值
}
}
#endregion #region 返回
/// <summary>
///
/// </summary>
protected override void Return()
{
//调用横切对象的OnExit方法
CallOn_Entry_Exit(OverrideMethodIL, false, AspectContext, AspectAttributes, OnEntryExit); if (HandleException)
{
AspectExceptionAttribute temp = AspectExceptionAttributes[] as AspectExceptionAttribute; OverrideMethodIL.BeginCatchBlock(typeof(Exception)); //保存Exception到临时变量
LocalBuilder exception = OverrideMethodIL.DeclareLocal(typeof(Exception)); OverrideMethodIL.Emit(OpCodes.Stloc, exception); //复制Exception到Result
var resultSetMethod = typeof(AspectContext).GetMethod("set_Result");
OverrideMethodIL.Emit(OpCodes.Ldloc, AspectContext); //加载AspectContext局部变量
OverrideMethodIL.Emit(OpCodes.Ldloc, exception);//错误信息
OverrideMethodIL.Emit(OpCodes.Box, typeof(Exception));
OverrideMethodIL.Emit(OpCodes.Call, resultSetMethod);//赋值 //调用OnEntry
LocalBuilder array = CreateAspectAttributeArray(OverrideMethodIL, AspectExceptionAttributes); CallOn_Entry_Exit(OverrideMethodIL, true, AspectContext, array, OnEntryExit); //触发Finally
if (temp.InterceptType.HasFlag(InterceptType.OnExit))
{
OverrideMethodIL.BeginFinallyBlock(); //调用Exit
array = CreateAspectAttributeArray(OverrideMethodIL, AspectExceptionAttributes); CallOn_Entry_Exit(OverrideMethodIL, false, AspectContext, array, OnEntryExit);
} OverrideMethodIL.EndExceptionBlock();
} base.Return();
}
#endregion
}
通过上边的代码我们可以看到,ILGenerateProxyMethod就拆分为一个类的三个方法,同时也灵活的处理了异常特性(if (HandleException),这里边新增了一个CallOn_Entry_Exit方法,其对应代码如下:
private static void CallOn_Entry_Exit(ILGenerator il_ProxyMethod, bool entry, LocalBuilder aspectContext, LocalBuilder array, MethodInfo onEntryExit)
{
il_ProxyMethod.Emit(OpCodes.Ldarg_0);
il_ProxyMethod.Emit(OpCodes.Ldstr, entry ? "OnEntry" : "OnExit");
il_ProxyMethod.Emit(OpCodes.Ldloc, aspectContext);
il_ProxyMethod.Emit(OpCodes.Ldloc, array); il_ProxyMethod.Emit(OpCodes.Call, onEntryExit);
}
#endregion
测试
测试类修改如下:
public class AopTest
{ public AopTest()
{
Name = "小明"; Age = ;
} public AopTest(string name, int age)
{
Name = name; Age = age;
} [Log(InterceptType.OnExit)]
public virtual string Name { get; set; } [Log(InterceptType.OnEntry)]
public virtual int Age { get; set; } [Log]
[AspectException]
public virtual int NYearLater(int a)
{
int larter = Age + a; return larter;
}
}
结果:
Log OnExit: set_Name Log OnEntry:set_Age() Log OnEntry:NYearLater()
Log OnExit: NYearLater Result:
Finally : NYearLater done...
比对结果,达到了我们的目的。
C# 使用Emit实现动态AOP框架 进阶篇之优化的更多相关文章
- C# 使用Emit实现动态AOP框架 进阶篇之异常处理
目 录 C# 使用Emit实现动态AOP框架 (一) C# 使用Emit实现动态AOP框架 (二) C# 使用Emit实现动态AOP框架 (三) C# 使用Emit实现动态AOP框架 进阶篇之异常处 ...
- C# 使用Emit实现动态AOP框架 (三)
目 录 C# 使用Emit实现动态AOP框架 (一) C# 使用Emit实现动态AOP框架 (二) C# 使用Emit实现动态AOP框架 (三) C# 使用Emit实现动态AOP框架 进阶篇之异常处 ...
- C# 使用Emit实现动态AOP框架 (二)
目 录 C# 使用Emit实现动态AOP框架 (一) C# 使用Emit实现动态AOP框架 (二) C# 使用Emit实现动态AOP框架 (三) C# 使用Emit实现动态AOP框架 进阶篇之异常处 ...
- C# 使用Emit实现动态AOP框架 (一)
目 录 C# 使用Emit实现动态AOP框架 (一) C# 使用Emit实现动态AOP框架 (二) C# 使用Emit实现动态AOP框架 (三) C# 使用Emit实现动态AOP框架 进阶篇之异常处 ...
- (6)MySQL进阶篇SQL优化(MyISAM表锁)
1.MySQL锁概述 锁是计算机协调多个进程或线程并发访问某一资源的机制.在数据库中,除传统的计算资源 (如 CPU.RAM.I/O 等)的抢占以外,数据也是一种供许多用户共享的资源.如何保证数 据并 ...
- hadoop之yarn详解(框架进阶篇)
前面在hadoop之yarn详解(基础架构篇)这篇文章提到了yarn的重要组件有ResourceManager,NodeManager,ApplicationMaster等,以及yarn调度作业的运行 ...
- 【Spring AOP】Spring AOP之如何通过注解的方式实现各种通知类型的AOP操作进阶篇(3)
一.切入点表达式的各种类型 切入点表达式的作用:限制连接点的匹配(满足时对应的aspect方法会被执行) 1)execution:用于匹配方法执行连接点.Spring AOP用户可能最经常使用exec ...
- (2)MySQL进阶篇SQL优化(show status、explain分析)
1.概述 在应用系统开发过程中,由于初期数据量小,开发人员写SQL语句时更重视功能上的实现,但是当应用系统正式上线后,随着生产数据量的急剧增长,很多SQL语句开始逐渐显露出性能问题,对生产环境的影响也 ...
- (3)MySQL进阶篇SQL优化(索引)
1.索引问题 索引是数据库优化中最常用也是最重要的手段之一,通过索引通常可以帮助用户解决大多数 的SQL性能问题.本章节将对MySQL中的索引的分类.存储.使用方法做详细的介绍. 2.索引的存储分类 ...
随机推荐
- token的解码及 判断值不为空的方法
token 的解码要使用插件:jwt-decode 判断值不为空的方法: function isEmpty(value){ return ( value === undefined || value ...
- code备忘
按空白符分隔(正则) String[] split = line.trim().split("\\s+");
- ctf密码学------密文解码python脚本(凯撒解密)
题目来源实验吧 分析题意,说是困在栅栏中,所以将字符栅栏解密看看有什么,利用工具CTFcraktools 得到三条密文 然后说是密码是凯撒,在将四栏依次凯撒解码,寻找可能的key,这里很显然,在尝试第 ...
- Python统计分析可视化库seaborn(相关性图,变量分布图,箱线图等等)
Visualization of seaborn seaborn[1]是一个建立在matplot之上,可用于制作丰富和非常具有吸引力统计图形的Python库.Seaborn库旨在将可视化作为探索和理 ...
- 利用百度智能云结合Python体验图像识别(转载来自qylruirui)
https://blog.csdn.net/qylruirui/article/details/94992917 利用百度智能云结合Python体验图像识别只要注册了百度账号就可以轻松体验百度智能云中 ...
- SpringBoot之解决一对一、多对一、多对多等关联实体在JSON序列化/输出时产生的无限递归死循环问题(infinite recursion)
前言 这问题着实让人苦不堪言,有必要把它记下了. @JsonBackReference [亲测有效] 1.使用注解@JsonBackReference标记在有关联关系的实体属性上 2.仅导入此注解类有 ...
- C#中的接口和抽象类学习
今天学习了接口和抽象类,但并没有很好的进行整理,所以现在写的时候,脑子里多少有点乱乱的,先从接口开始吧. interface 接口,规定了所有派生类的需要遵循的标准,接口定义了需要做些什么,但是没有具 ...
- nginx+gunicorn拓扑
加装Nginx是否有必要 Nginx作为代理服务器,监听来自外部的80端口的请求:而Gunicorn负责监听本地机8000端口的请求.Nginx会把来自外部的请求转发给Gunicorn处理,接收到响应 ...
- Leetcode之动态规划(DP)专题-714. 买卖股票的最佳时机含手续费(Best Time to Buy and Sell Stock with Transaction Fee)
Leetcode之动态规划(DP)专题-714. 买卖股票的最佳时机含手续费(Best Time to Buy and Sell Stock with Transaction Fee) 股票问题: 1 ...
- kube-metric在kubernetes上的部署
1.拿包 wgethttps://github.com/kubernetes/kube-state-metrics/archive/v1.7.2.tar.gz 2.tar -zxf v1.7.2.t ...