CGLib 简析
背景
JDK 动态代理存在的一些问题:
调用效率低
JDK 通过反射实现动态代理调用,这意味着低下的调用效率:
每次调用
Method.invoke()
都会检查方法的可见性、校验参数是否匹配,过程涉及到很多 native 调用,具体参见JNI 调用开销反射调用涉及动态类解析,这种不可预测性,导致被反射调用的代码无法被 JIT 内联优化,具体参见反射调用方法
可以通过java.lang.invoke.MethodHandle
来规避以上问题,但是这不在本文讨论的范围。
只能代理接口
java.lang.reflect.Proxy
只支持通过接口生成代理类,这意味着 JDK 动态代理只能代理接口,无法代理具体的类。
对于一些外部依赖或者现有模块来说,无法通过该方式实现动态代理。
应用场景
CGLib 是一款用于实现高效动态代理的字节码增强库,通过字节码生成技术,动态编译生成代理类,从而将反射调用转换为普通的方法调用。
下面通过两个案例体验一下 CGLib 的使用方式。
案例一:Weaving
现有一个输出问候语句的类 Greet
,现在有个新需求:在输出内容前后加上姓名,实现个性化输出。下面通过 CGLib 实现该功能:
class Greet { // 需要被增强目标类
public String hello() { return "hello"; }
public String hi() { return "hi"; }
public String toString() { return "@Greet"; }
}
public class Weaving { // 模拟切面织入过程
// 增加 before: 前缀(模拟前置通知)
static MethodInterceptor adviceBefore = (target, method, args, methodProxy) -> "before:" + methodProxy.invokeSuper(target, args);
// 增加 :after 后缀(模拟后置通知)
static MethodInterceptor adviceAfter = (target, method, args, methodProxy) -> methodProxy.invokeSuper(target, args) + ":after";
// 通知
static Callback[] advices = new Callback[] { NoOp.INSTANCE/*默认*/, adviceBefore, adviceAfter };
// 切入点
static CallbackFilter pointCut = method -> {
switch (method.getName()) {
case "hello" : return 1; // hello() 方法植入前置通知
case "hi" : return 2; // hi() 方法植入后置通知
default: return 0; // 其他方法不添加通知
}
};
public static void main(String[] args) throws InterruptedException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Greet.class); // 设置目标类
enhancer.setCallbacks(advices); // 设置通知 Advice
enhancer.setCallbackFilter(pointCut); // 设置切点 PointCut
Greet greet = (Greet) enhancer.create(); // 创建 Proxy 对象
System.out.println(greet.hello());
System.out.println(greet.hi());
System.out.println(greet.toString());
TimeUnit.HOURS.sleep(1);
}
}
案例二:Introduction
随着业务发展,系统需要支持法语的问候 FranceGreet
,在不修改现有业务代码的前提下,可以通过 CGLib 实现该功能:
interface FranceGreet { // 支持新功能的接口
String bonjour();
}
class FranceGreeting implements Dispatcher { // 新接口的实现
private final FranceGreet delegate = () -> "bonjour";
@Override
public Object loadObject() throws Exception {
return delegate;
}
}
class FranceGreetingMatcher implements CallbackFilter { // 将新接口调用委托给 Dispatcher
@Override
public int accept(Method method) {
return method.getDeclaringClass().equals(FranceGreet.class) ? 1 : 0;
}
}
public class Introduction { // 模拟引入新接口
public static void main(String[] args) throws InterruptedException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Greet.class);
enhancer.setInterfaces(new Class[]{FranceGreet.class}); // 扩展新接口
enhancer.setCallbacks(new Callback[]{ NoOp.INSTANCE, new FranceGreeting()}); // 实现新接口
enhancer.setCallbackFilter(new FranceGreetingMatcher()); // 关联接口与实现
Greet greet = (Greet) enhancer.create();
System.out.println(greet.hello()); // 原方法不受影响
System.out.println(greet.hi());
FranceGreet franceGreet = (FranceGreet) greet;
System.out.println(franceGreet.bonjour()); // 新接口方法正常调用
TimeUnit.HOURS.sleep(1);
}
}
原理简析
从前面的案例可以看到,CGLib 使用的方式很简单,大致可以分为两步:
- 配置 Enhancer
- 设置需要代理的目标类与接口
- 通过
Callback
设置需要增强的功能 - 通过
CallbackFilter
将方法匹配到具体的Callback
- 创建代理对象
- 通过
CallbackFilter
获取方法与Callback
的关联关系 - 继承目标类并重写
override
方法,在调用代码中嵌入 Callback - 编译动态生成的字节码生成代理类
- 通过反射调用构造函数生成代理对象
Callback 分类
此外,CGLib 支持多种 Callback,这里简单介绍几种:
NoOp
不使用动态代理,匹配到的方法不会被重写FixedValue
返回固定值,被代理方法的返回值被忽略Dispatcher
指定上下文,将代理方法调用委托给特定对象MethodInterceptor
调用拦截器,用于实现环绕通知around advice
其中 MethodInterceptor
最为常用,可以实现多种丰富的代理特性。
但这类 Callback
也是其中最重的,会导致生成更多的动态类,具体原因后续介绍。
字节码生成过程
底层通过 Enhancer.generateClass()
生成代理类,其具体过程不作深究,可以简单概括为:
- 通过
ClassVisitor
获取目标类信息 - 通过
ClassEmitter
调用 asm 库注入增强方法,并生成byte[]
形式的字节码 - 通过反射调用
ClassLoader.defineClass()
将byte[]
转换为Class
对象 - 将生成完成的代理类缓存至
LoadingCache
,避免重复生成
生成的类结构
通过 arthas 的 jad 命令可以观察到,案例 Weaving 中实际生成了以下类:
- 目标类:buttercup.test.Greet
- 代理类:buttercup.test.Greet$$EnhancerByCGLIB(省略后缀)
- 目标类 FastClass:buttercup.test.Greet$$FastClassByCGLIB(省略后缀)
- 代理类 FastClass:buttercup.test.Greet$$EnhancerByCGLIB$$FastClassByCGLIB(省略后缀)
代理类
代理类就是 Ehancer.create()
中为了创建代理对象动态生成的类,该类不但继承了目标类,并且还重写了需要被代理的方法。其命名规则为:目标类 + $$EnhancerByCGLIB。
在案例一中,我们分别给 Greet.hello()
与 Greet.hi()
分别添加了拦截器Weaving.adviceBefore
与 Weaving.adviceAfter
,下面我们分析代理类是如何完成这一功能的:
public class Greet$$EnhancerByCGLIB extends Greet implements Factory {
private static final Object[] CGLIB$emptyArgs = new Object[0]; // 默认空参数
private static final Callback[] CGLIB$STATIC_CALLBACKS; // 静态 Callback(忽略)
private static final ThreadLocal CGLIB$THREAD_CALLBACKS; // 用于给构造函数传递 Callback
// 通过 MethodProxy 代理 Greet.hell() 方法
private static final Method CGLIB$hello$0$Method;
private static final MethodProxy CGLIB$hello$0$Proxy;
// 通过 MethodProxy 代理 Greet.hi() 方法
private static final Method CGLIB$hi$1$Method;
private static final MethodProxy CGLIB$hi$1$Proxy;
private boolean CGLIB$BOUND; // 判断 Callback 是否已经初始化
private NoOp CGLIB$CALLBACK_0; // 默认不拦截,直接调用目标类方法
private MethodInterceptor CGLIB$CALLBACK_1; // Weaving.adviceBefore(增加 before: 前缀)
private MethodInterceptor CGLIB$CALLBACK_2; // Weaving.adviceAfter(增加 :after 后缀)
static {
Greet$$EnhancerByCGLIB.CGLIB$STATICHOOK1();
}
static void CGLIB$STATICHOOK1() { // 静态初始化
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
Class<?> clazz = Class.forName("buttercup.test.Greet");
Class<?> clazz2 = Class.forName("buttercup.test.Greet$$EnhancerByCGLIB");
Method[] methodArray = ReflectUtils.findMethods(new String[]{"hello", "()Ljava/lang/String;", "hi", "()Ljava/lang/String;"}, clazz.getDeclaredMethods());
CGLIB$hello$0$Method = methodArray[0];
CGLIB$hello$0$Proxy = MethodProxy.create(clazz, clazz2, "()Ljava/lang/String;", "hello", "CGLIB$hello$0");
CGLIB$hi$1$Method = methodArray[1];
CGLIB$hi$1$Proxy = MethodProxy.create(clazz, clazz2, "()Ljava/lang/String;", "hi", "CGLIB$hi$1");
}
// 通过 ThreadLocal 传参
public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] callbackArray) {
CGLIB$THREAD_CALLBACKS.set(callbackArray);
}
// 工厂方法,创建增强后的对象
public Object newInstance(Callback[] callbackArray) {
Greet$$EnhancerByCGLIB.CGLIB$SET_THREAD_CALLBACKS(callbackArray);
Greet$$EnhancerByCGLIB Greet$$EnhancerByCGLIB = new Greet$$EnhancerByCGLIB();
Greet$$EnhancerByCGLIB.CGLIB$SET_THREAD_CALLBACKS(null);
return Greet$$EnhancerByCGLIB;
}
// 重写 hello() 方法通知 CALLBACK_1
public final String hello() {
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_1;
if (methodInterceptor == null) { // 初始化 CALLBACK_1
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
methodInterceptor = this.CGLIB$CALLBACK_1;
}
if (methodInterceptor != null) { // 调用拦截器 Weaving.adviceBefore
return (String)methodInterceptor.intercept(this, CGLIB$hello$0$Method, CGLIB$emptyArgs, CGLIB$hello$0$Proxy);
}
return super.hello();
}
// 重写 hi() 方法通知 CALLBACK_2
public final String hi() {
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_2;
if (methodInterceptor == null) { // 初始化 CALLBACK_2
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
methodInterceptor = this.CGLIB$CALLBACK_2;
}
if (methodInterceptor != null) { // 调用拦截器 Weaving.adviceAfter
return (String)methodInterceptor.intercept(this, CGLIB$hi$1$Method, CGLIB$emptyArgs, CGLIB$hi$1$Proxy);
}
return super.hi();
}
// 直接调用目标类的 hello()
final String CGLIB$hello$0() {
return super.hello();
}
// 直接调用目标类的 hi()
final String CGLIB$hi$1() {
return super.hi();
}
public Greet$$EnhancerByCGLIB() {
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
}
private static final void CGLIB$BIND_CALLBACKS(Object object) {
block2: {
Object object2;
Greet$$EnhancerByCGLIB Greet$$EnhancerByCGLIB;
block3: {
Greet$$EnhancerByCGLIB = (Greet$$EnhancerByCGLIB) object;
// 如果已经初始化过,则直接返回
if (Greet$$EnhancerByCGLIB.CGLIB$BOUND) break block2;
Greet$$EnhancerByCGLIB.CGLIB$BOUND = true;
if (object2 = CGLIB$THREAD_CALLBACKS.get()) != null) break block3; // 从 ThreadLocal 获取 Callback 参数
if ((object2 = CGLIB$STATIC_CALLBACKS) == null) break block2;
}
Callback[] callbackArray = (Callback[])object2; // 初始化 Callback 参数
Greet$$EnhancerByCGLIB greet$$EnhancerByCGLIB = Greet$$EnhancerByCGLIB;
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_2 = (MethodInterceptor)callbackArray[2];
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_1 = (MethodInterceptor)callbackArray[1];
greet$$EnhancerByCGLIB.CGLIB$CALLBACK_0 = (NoOp)callbackArray[0];
}
}
}
在动态生成的类中,可以看到 CGLib 为每个被代理的方法创建了 MethodProxy
对象。
该对象替代了 Method.invoke()
功能,是实现高效方法调用的的关键。下面我们以 Greet.hello()
为例对该类进行分析:
public class MethodProxy {
private Signature sig1; // 目标类方法签名:hello()Ljava/lang/String;
private Signature sig2; // 代理类方法签名:CGLIB$hello$0()Ljava/lang/String;
private MethodProxy.CreateInfo createInfo; /* 省略初始化过程 */
private static class CreateInfo {
Class c1; // 目标类 buttercup.test.Greet
Class c2; // 代理类 buttercup.test.Greet$$EnhancerByCGLIB
}
private final Object initLock = new Object();
private volatile MethodProxy.FastClassInfo fastClassInfo;
private static class FastClassInfo {
FastClass f1; // 目标类 FastClass :
FastClass f2; // 代理类 FastClass :
int i1; // 方法在 f1 中对应的索引
int i2; // 方法在 f2 中对应的索引
}
// 只在 MethodProxy 被调用时加载 FastClass,减少不必要的类生成(lazy-init)
private void init() {
if (fastClassInfo == null) {
synchronized (initLock) {
if (fastClassInfo == null) {
MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
fci.f1 = helper(ci, ci.c1); //
fci.f2 = helper(ci, ci.c2); //
fci.i1 = fci.f1.getIndex(this.sig1);
fci.i2 = fci.f2.getIndex(this.sig2);
fastClassInfo = new FastClassInfo();
}
}
}
}
// 根据 Class 对象生成 FastClass
private static FastClass helper(MethodProxy.CreateInfo ci, Class type) {
Generator g = new Generator();
g.setType(type);
return g.create();
}
// 调用 buttercup.test.Greet.hello()
// 但实际上会调用代理类的 EnhancerByCGLIB.hello() 实现
public Object invoke(Object obj, Object[] args) throws Throwable {
init();
return fastClassInfo.f1.invoke(fci.i1, obj, args);
}
// 调用 buttercup.test.Greet$$EnhancerByCGLIB.CGLIB$hello$0()
// 通过 super.hello() 调用目标类的 Greet.hello() 实现
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
init();
return fastClassInfo.f2.invoke(fci.i2, obj, args);
}
}
可以看到 MethodProxy
的调用实际是通过 FastClass
完成的,这是 CGLib 实现高性能反射调用的秘诀,下面来解析这个类的细节。
目标类 FastClass
为了规避反射带来的性能消耗,cglib 定义了 FastClass
来实现高效的方法调用,其主要职责有两个
- 方法映射:解析 Class 对象并为每个 Constructor 与 Method 指定一个整数索引值 index
- 方法调用:通过 switch(index) 的方式,将反射调用转化为硬编码调用
abstract public class FastClass {
// 映射:根据方法名称与参数类型,获取其对应的 index
public abstract int getIndex(String methodName, Class[] argClass);
// 调用:根据 index 找到指定的方法,并进行调用
public abstract Object invoke(int index, Object obj, Object[] args) throws InvocationTargetException;
}
其命名规则为:目标类 + $$FastClassByCGLIB。下面具体分析一下对目标类 Greet
对应的 FastClass
:
public class Greet$$FastClassByCGLIB extends FastClass {
public Greet$$FastClassByCGLIB(Class clazz) {
super(clazz);
}
// 获取 index 的最大值
public int getMaxIndex() {
return 4; // 当前 FastClass 总共支持 5 个方法
//索引值分别为 0:hello(), 1:hi(), 2:equals(), 3:hasCode(), 4:toString()
}
// 根据方法名称以及参数类型,获取到指定方法对应的 index
public int getIndex(String methodName, Class[] argClass) {
switch (methodName.hashCode()) {
case 3329: {
if (!methodName.equals("hi")) break;
switch (argClass.length) {
case 0: { return 1; } // hi() 对应 index 为 1
}
break;
}
case 99162322: {
if (!methodName.equals("hello")) break;
switch (argClass.length) {
case 0: { return 0; } // hello() 对应 index 为 0
}
break;
}
/* 忽略 Object 方法 */
}
return -1;
}
// 根据方法签名,获取到指定方法对应的 index
public int getIndex(Signature signature) {
String sig = ((Object)signature).toString();
switch (sig.hashCode()) {
case 397774237: {
if (!sig.equals("hello()Ljava/lang/String;")) break;
return 0; // hello() 对应 index 为 0
}
case 1155503180: {
if (!sig.equals("hi()Ljava/lang/String;")) break;
return 1; // hi() 对应 index 为 1
}
/* 忽略 Object 方法 */
}
return -1;
}
// 方法调用(硬编码调用)
public Object invoke(int n, Object obj, Object[] args) throws InvocationTargetException {
Greet greet = (Greet) obj;
switch (n) { // 通过 index 指定目标函数
case 0: { return greet.hello(); } // 通过索引 0 调用 hello()
case 1: { return greet.hi(); } // 通过索引 1 调用 hi()
/* 忽略 Object 方法 */
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
// 构造函数(硬编码调用)
public Object newInstance(int n, Object[] argClass) throws InvocationTargetException {
switch (n) {
case 0: { return new Greet(); }
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
}
代理类 FastClass
之前提及过:使用 MethodInterceptor
会比其他 Callback
生成更多的动态类,这是因为需要支持 MethodProxy.invokeSuper()
调用:
public interface MethodInterceptor extends Callback {
// 所有生成的代理方法都调用此方法
// 大多数情况需要通过 MethodProxy.invokeSuper() 来实现目标类的调用
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
MethodProxy.invokeSuper()
通过调用代理类中带 $CGLIB$
前缀的方法,绕过被重写的代理方法,避免出现无限递归。
为了保证调用效率,需要对代理类也生成 FastClass
:
public class Greet$$EnhancerByCGLIB$$FastClassByCGLIB extends FastClass {
public Object invoke(int n, Object obj, Object[] args) throws InvocationTargetException {
Greet$$EnhancerByCGLIB greet$$EnhancerByCGLIB = (Greet$$EnhancerByCGLIB)obj;
switch (n) {
case 9: { // 调用目标类的原始 Greet.hello() 方法
return greet$$EnhancerByCGLIB.CGLIB$hello$0();
}
case 10: { // 调用目标类的原始 Greet.hi() 方法
return greet$$EnhancerByCGLIB.CGLIB$hi$1();
}
/* 忽略其他 Enhancer 方法 */
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
public int getIndex(String methodName, Class[] argClass) {
switch (methodName.hashCode()) {
case 1837078673: {
if (!methodName.equals("CGLIB$hi$1")) break;
switch (argClass.length) {
case 0: { return 10; }
}
break;
}
case 1891304123: {
if (!methodName.equals("CGLIB$hello$0")) break;
switch (argClass.length) {
case 0: { return 9; }
}
break;
}
/* 忽略其他 Enhancer 方法 */
}
return -1;
}
public int getIndex(Signature signature) {
String sig = ((Object)signature).toString();
switch (sig.hashCode()) {
case -1632605946: {
if (!sig.equals("CGLIB$hello$0()Ljava/lang/String;")) break;
return 9;
}
case 540391388: {
if (!sig.equals("CGLIB$hi$1()Ljava/lang/String;")) break;
return 10;
}
/* 忽略其他 Enhancer 方法 */
}
return -1;
}
}
补充
案例 Introduction 中仅使用了 Dispatcher
,因此只生成了代理类,未使用到 FastClass
:
public class Greet$$EnhancerByCGLIB extends Greet implements FranceGreet, Factory {
private NoOp CGLIB$CALLBACK_0;
private Dispatcher CGLIB$CALLBACK_1;
public final String bonjour() {
Dispatcher dispatcher = this.CGLIB$CALLBACK_1;
if (dispatcher == null) {
Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
dispatcher = this.CGLIB$CALLBACK_1;
}
return ((FranceGreet)dispatcher.loadObject()).bonjour();
}
/* 忽略多余的属性与方法 */
}
本文案例仅涉及 MethodInterceptor
与 Dispatcher
,这两个 Callback 也是 Spring AOP 实现的关键,后续将继续分析相关的源码实现。
附录
CGLib 简析的更多相关文章
- 简析.NET Core 以及与 .NET Framework的关系
简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...
- 简析 .NET Core 构成体系
简析 .NET Core 构成体系 Roslyn 编译器 RyuJIT 编译器 CoreCLR & CoreRT CoreFX(.NET Core Libraries) .NET Core 代 ...
- RecycleView + CardView 控件简析
今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...
- Java Android 注解(Annotation) 及几个常用开源项目注解原理简析
不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...
- PHP的错误报错级别设置原理简析
原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...
- Android 启动过程简析
首先我们先来看android构架图: android系统是构建在linux系统上面的. 所以android设备启动经历3个过程. Boot Loader,Linux Kernel & Andr ...
- Android RecycleView + CardView 控件简析
今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...
- Java Annotation 及几个常用开源项目注解原理简析
PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...
- 【ACM/ICPC2013】POJ基础图论题简析(一)
前言:昨天contest4的惨败经历让我懂得要想在ACM领域拿到好成绩,必须要真正的下苦功夫,不能再浪了!暑假还有一半,还有时间!今天找了POJ的分类题库,做了简单题目类型中的图论专题,还剩下二分图和 ...
随机推荐
- Dubbo 实现一个Route Factory(用于灰度发布)
Dubbo 可以实现的扩展很多, 官方文档在这: https://dubbo.apache.org/zh/docs/v2.7/dev/impls/ (太简单了....) 下面我们实现一个Route F ...
- 在Linearlayout中新增ScrollView支持滚动
https://blog.csdn.net/wenzhi20102321/article/details/53491176 1.一般只需要在布局中加个ScrollView即可 2.如果布局中包含lis ...
- CCS box-flex属性
box-flex==按比例分配父标签的宽度or高度空间 1.非固定分配 eg.一块地总150平方分配给三孩子,按照2:1:1分 #老大 { 房子-分配: 2; } = 75平 #老二 { 房子-分配: ...
- How to name a slf4j logger
Use logger in a non-static context: Logger logger = LoggerFactory.getLogger(this.getClass().getName( ...
- rabbitMQ通过@RabbitListener和配置文件XML创建exchange及队列绑定关系
1.@RabbitListener 2.配置文件 <rabbit:fanout-exchange name="fanoutExchange" xmlns="http ...
- 【笔记】浅谈支持向量机(SVM)
SVM支持向量机 支持向量机的思想原理 使用支持向量机的思想,既可以解决回归问题,又可以解决分类问题 那么支持向量机的思想是什么? 已经知道逻辑回归这种算法的本质就是在一个平面中寻找决策边界,而分类算 ...
- Activity与Service生命周期
一. Activity 先展示一张Activity的生命周期图: 1.1 Activity状态 只有下面三个状态是静态的,可以存在较长的时间内保持状态不变.(其它状态只是过渡状态,系统快速执行并切换到 ...
- 新版数据库分页方法(Sql server2012)
1. ROW_NUMBER() 的分页方法 dbcc freeproccache dbcc dropcleanbuffers set statistics time on set statistics ...
- kubernetes/k8s CNI分析-容器网络接口分析
关联博客:kubernetes/k8s CSI分析-容器存储接口分析 kubernetes/k8s CRI分析-容器运行时接口分析 概述 kubernetes的设计初衷是支持可插拔架构,从而利于扩展k ...
- 你真的熟悉ASP.NET MVC的整个生命周期吗?
一.介绍 我们做开发的,尤其是做微软技术栈的,有一个方向是跳不过去的,那就是MVC开发.我相信大家,做ASP.NET MVC 开发有的有很长时间,当然,也有刚进入这个行业的.无论如何,如果有人问你,你 ...