zoukankan      html  css  js  c++  java
  • 代理模式

    代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

    优点:在不修改原来代码的情况下增加自己的功能,比如记录日志等

    静态代理

    接口类

    public interface Car {
        void run();
    }
    

    需要被代理的类(实现类)

    public class BenzCar implements Car {
    
        @Override
        public void run() {
            System.out.println("奔驰run了");
        }
    }
    
    

    代理类(静态代理类)

    public class BenzCarImplProxy implements Car {
    
        private Car car;
    
        public BenzCarImplProxy(Car car) {
            this.car = car;
        }
    
        @Override
        public void run() {
            System.out.println("秋名山道路畅通,准备发车了");
            car.run();
            System.out.println("五菱宏光果然厉害");
        }
    }
    

    测试

    public static void main(String[] args) {
        Car car = new BenzCarImplProxy(new BenzCar());
        car.run();
    }
    

    我们发现静态代理是在编译阶段就已经指定好了被代理的接口对象,即只能对某种类型(是一个接口)代理,如果是有多种类型,那我们就要写很多套这样的代码,想必是及其不好的。

    动态代理

    JDK动态代理

    jdk的动态代理只能代理接口

    示例:

    接口

    public interface Car {
        void run();
    }
    

    接口的实现类

    public class CarImpl implements Car {
    
        @Override
        public void run() {
            System.out.println("道路无阻碍...");
        }
    }
    

    InvocationHandler实现类

    /**
     * Create by IntelliJ Idea 2018.2
     *
     * 动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。
     * 该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类
     * InvocationHandler其实就是一个回调接口
    
     *  注意:对于从Object中继承的方法,JDK Proxy会把hashCode()、equals()、toString()这三个非接口方法转发给InvocationHandler,其余的Object方法则不会转发
     * 
     * @author: qyp
     * Date: 2019-07-11 14:04
     */
    public class ProxyImplHandler implements InvocationHandler {
    
        /**
         * 目标对象
         */
        private Object targetObj;
    
        public Object newProxyInstance(Object targetObj) {
            this.targetObj = targetObj;
            /**
             * 根据传入的目标返回一个代理对象
             *
             * param1: 指定代理对象的类加载器,需要将其指定为和目标对象同一个类加载器
             * param2: 代理对象需要实现的接口,可以同时指定多个接口(因为代理类要实现和目标对象一样的接口)
             *          目标类的方法在代理类中必须有
             *
             * param3: 代理类被调用后,执行invoke方法的目标对象
             *         (方法调用的实际处理者,代理对象的方法调用都会转发到这里(*注意))
             *
             */
            return Proxy.newProxyInstance(targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(), this);
        }
    
        /**
         * 关联的这个实现类的方法被调用时将被执行
         * @param proxy  代理类
         * @param method  被调用的方法
         * @param args    方法参数
         * @return
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("老司机发车了。。。");
            Object result = method.invoke(targetObj, args);
            System.out.println("五菱宏光太牛逼了");
            return result;
        }
    }
    

    调用

    public class Client {
    
        public static void main(String[] args) {
    
            // 设置该属性,可以生成代理类的class文件
            System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
            ProxyImplHandler proxyImplHandler = new ProxyImplHandler();
            Car proxyCar = (Car) proxyImplHandler.newProxyInstance(new CarImpl());
            proxyCar.run();
    
        }
    }
    

    看一下生成的代理类

    package com.sun.proxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    import per.qiao.pattern.proxy.Car;
    
    /**
     * 继承了Proxy对象 实现了目标接口
     */
    public final class $Proxy0 extends Proxy implements Car {
        private static Method m1;
        private static Method m3;
        private static Method m2;
        private static Method m0;
    
        public $Proxy0(InvocationHandler var1) throws  {
            super(var1);
        }
        
    	// 这里实现的
        public final void run() throws  {
            try {
                super.h.invoke(this, m3, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
        public final boolean equals(Object var1) throws  {
            try {
                return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
                throw var3;
            } catch (Throwable var4) {
                throw new UndeclaredThrowableException(var4);
            }
        }
    
        public final String toString() throws  {
            try {
                return (String)super.h.invoke(this, m2, (Object[])null);
            } catch ...
        }
    
        public final int hashCode() throws  {
            try {
                return (Integer)super.h.invoke(this, m0, (Object[])null);
            } catch ...
        }
    
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
                m3 = Class.forName("per.qiao.pattern.proxy.Car").getMethod("run");
                m2 = Class.forName("java.lang.Object").getMethod("toString");
                m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch ...
        }
    }
    

    源码分析

    public static Object newProxyInstance(ClassLoader loader,
                                              Class<?>[] interfaces,
                                              InvocationHandler h)
            throws IllegalArgumentException
        {
            Objects.requireNonNull(h);
    
            final Class<?>[] intfs = interfaces.clone();
            final SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
            	//如果调用方的类加载器与接口的定义加载器不同,则当通过defineclass0方法定义生成的代理类时,VM将抛出IllegalAccessError。
                checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
            }
    
            /*
             * Look up or generate the designated proxy class.
             */
            Class<?> cl = getProxyClass0(loader, intfs);
    
            /*
             * Invoke its constructor with the designated invocation handler.
             */
            try {
                if (sm != null) {
                    checkNewProxyPermission(Reflection.getCallerClass(), cl);
                }
    
                final Constructor<?> cons = cl.getConstructor(constructorParams);
                final InvocationHandler ih = h;
                if (!Modifier.isPublic(cl.getModifiers())) {
                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                        public Void run() {
                            cons.setAccessible(true);
                            return null;
                        }
                    });
                }
                // 返回实例化对象 参数为InvocationHandler对象
                return cons.newInstance(new Object[]{h});
            } catch ...
              
        }
    
    
    private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
            proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
    
    private static Class<?> getProxyClass0(ClassLoader loader,
                                               Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }
    
        //如果给定接口和加载器对应的代理类存在,那么从缓存中直接获取
        return proxyClassCache.get(loader, interfaces);
    }
    

    WeakCache

        public V get(K key, P parameter) {
            Objects.requireNonNull(parameter);
    
            expungeStaleEntries();
    
            Object cacheKey = CacheKey.valueOf(key, refQueue);
    
            // lazily install the 2nd level valuesMap for the particular cacheKey
            ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
            if (valuesMap == null) {
                ConcurrentMap<Object, Supplier<V>> oldValuesMap
                    = map.putIfAbsent(cacheKey,
                                      valuesMap = new ConcurrentHashMap<>());
                if (oldValuesMap != null) {
                    valuesMap = oldValuesMap;
                }
            }
    
            // 创建key  subKeyFactory是上面的 new KeyFactory()
            Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
            // 先从缓存中取
            Supplier<V> supplier = valuesMap.get(subKey);
            Factory factory = null;
    
            while (true) {
                if (supplier != null) {
                    // 可能是在facotry或者 缓存中获取
                    // 第一次创建从Factory中获取
                    V value = supplier.get();
                    if (value != null) {
                        return value;
                    }
                }
                // 缓存中没有,创建Factory 
                if (factory == null) {
                    factory = new Factory(key, parameter, subKey, valuesMap);
                }
    			//添加到缓存
                if (supplier == null) {
                    supplier = valuesMap.putIfAbsent(subKey, factory);
                    if (supplier == null) {
                        // successfully installed Factory
                        supplier = factory;
                    }
                    // else retry with winning supplier
                } else {
                    //创建factroy成功后,替换之前的缓存(如果之前有缓存结果)
                    if (valuesMap.replace(subKey, supplier, factory)) {
                        // successfully replaced
                        // cleared CacheEntry / unsuccessful Factory
                        // with our Factory
                        supplier = factory;
                    } else {
                        // 获取缓存对象或者当前创建的factory
                        supplier = valuesMap.get(subKey);
                    }
                }
            }
        }
    

    Factory

    public synchronized V get() { // serialize access
        // 再次校验
        Supplier<V> supplier = valuesMap.get(subKey);
        if (supplier != this) {
            // 如果valuesMap中的,supplier对象被修改过,那么重新循环获取,直到缓存中的supplier和当前Factroy是同一个对象为止
            return null;
        }
        // 直到 (supplier == this(factory))
    
        // 创建value
        V value = null;
        try {
            // 调用ProxyClassFactory的apply方法 获取到代理对象
            value = Objects.requireNonNull(valueFactory.apply(key, parameter));
        } finally {
            if (value == null) { // 创建失败,清除缓存
                valuesMap.remove(subKey, this);
            }
        }
        // the only path to reach here is with non-null value
        assert value != null;
    
        // wrap value with CacheValue (WeakReference)
        CacheValue<V> cacheValue = new CacheValue<>(value);
    
        // 用当前的supplier对像替换掉与当前不同对象的缓存
        if (valuesMap.replace(subKey, this, cacheValue)) {
            // put also in reverseMap
            reverseMap.put(cacheValue, Boolean.TRUE);
        } else {
            throw new AssertionError("Should not reach here");
        }
    
        // successfully replaced us with new CacheValue -> return the value
        // wrapped by it
        return value;
    }
    

    ProxyClassFactory

    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
    
        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) {
            //验证类加载器是否将此接口的名称解析为同一Class对象。
            Class<?> interfaceClass = null;
            try {
                // 使用传递过进来的类加载器加载 当前接口
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            //如果当前传递的类加载器加载的类型与传递的接口类型不同则抛异常
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
            // 传递的必须是接口
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            //验证此接口不是重复的。 IdentityHashMap是通过==来比较key是否相同的
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }
    
        String proxyPkg = null;     // package to define proxy class in
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
    
        /*
                 * Record the package of a non-public proxy interface so that the
                 * proxy class will be defined in the same package.  Verify that
                 * all non-public proxy interfaces are in the same package.
                 */
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }
    	//代理类所在的包 com.sun.proxy
        if (proxyPkg == null) {
            // if no non-public proxy interfaces, use com.sun.proxy package
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }
    
        //选择要生成的代理类的名称 com.sun.proxy.$Proxy0[N]
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;
    
        //生成代理类的字节码
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            // 生成代理类的class对象
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
                     * A ClassFormatError here means that (barring bugs in the
                     * proxy class generation code) there was some other
                     * invalid aspect of the arguments supplied to the proxy
                     * class creation (such as virtual machine limitations
                     * exceeded).
                     */
            throw new IllegalArgumentException(e.toString());
        }
    }
    

    CGLI动态代理

    写个测试

    BaseHello

    public interface BaseHello {
        void say();
    }
    

    HelloSbu

    public class HelloSub implements BaseHello {
    
        @Override
        public void say() {
            System.out.println("hello world!!!");
        }
    }
    

    MyMethodInterceptor

    顾名思义,拦截了所有的调用方法

    public class MyMethodInterceptor implements MethodInterceptor {
    
        /**
         * @param proxyObj cglib生成的代理对象
         * @param method 被代理的方法
         * @param args 方法参数
         * @param methodProxy 代理方法
         * @return
         * @throws Throwable
         */
        @Override
        public Object intercept(Object proxyObj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("======插入前置通知======");
            Object object = methodProxy.invokeSuper(proxyObj, args);
            System.out.println("======插入后者通知======");
            return object;
        }
    }
    
    public static void main(String[] args) {
    
            // 代理类class文件存入本地磁盘方便我们反编译查看源码
            System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\Java4IDEA\comm_test\com\sun\proxy");
    
            // 通过CGLIB动态代理获取代理对象的过程
            Enhancer enhancer = new Enhancer();
    
            //代理具体类型,
            enhancer.setSuperclass(HelloSub.class);
            //代理接口(其实不用指定接口)
            enhancer.setInterfaces(new Class[]{BaseHello.class});
    
            // 设置enhancer的回调对象
            enhancer.setCallback(new MyMethodInterceptor());
    
    
            BaseHello baseHello = (BaseHello) enhancer.create();
            // 通过代理对象调用目标方法
            baseHello.say();
        }
    

    这里写了父类,同时也加上了接口

    生成的文件大致如下(奇丑无比)

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package per.qiao.pattern.proxy.dynamicproxy.cglib_dynamicproxy;
    
    import java.lang.reflect.Method;
    import net.sf.cglib.core.ReflectUtils;
    import net.sf.cglib.core.Signature;
    import net.sf.cglib.proxy.Callback;
    import net.sf.cglib.proxy.Factory;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    public class HelloSub$$EnhancerByCGLIB$$dcdd7f9 extends HelloSub implements BaseHello, Factory {
        
        private MethodInterceptor methodInterceptor;
        
        public final void say() {
            MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
            if (this.CGLIB$CALLBACK_0 == null) {
                CGLIB$BIND_CALLBACKS(this);
                var10000 = this.CGLIB$CALLBACK_0;
            }
    
            if (var10000 != null) {
                methodInterceptor.intercept(this, CGLIB$say$0$Method, CGLIB$emptyArgs, CGLIB$say$0$Proxy);
            } else {
                super.say();
            }
        }
    
        public final boolean equals(Object var1) {
            return methodInterceptor.intercept(this, CGLIB$equals$1$Method, new Object[]{var1}, CGLIB$equals$1$Proxy);
        }
    
        public final String toString() {
            return (String)methodInterceptor.intercept(this, CGLIB$toString$2$Method, CGLIB$emptyArgs, CGLIB$toString$2$Proxy)
        }
    
        public final int hashCode() {
            return methodInterceptor.intercept(this, CGLIB$hashCode$3$Method, CGLIB$emptyArgs, CGLIB$hashCode$3$Proxy)
        }
    
    
        protected final Object clone() throws CloneNotSupportedException {
            return methodInterceptor.intercept(this, CGLIB$clone$4$Method, CGLIB$emptyArgs, CGLIB$clone$4$Proxy)
        }
    }
    
    

    我们发现都是执行了MethodInterceptor接口中的intercept方法

    下面分析一下部分源码(源码不复杂,代码量比较少,建议自己查看)

    AbstractClassGenerator

    protected Object create(Object key) {
        try {
            //获得类加载器
            ClassLoader loader = getClassLoader();
            // CACHE是一个静态Map,cglib生成动态类的类加载器都存放在这儿
            Map<ClassLoader, ClassLoaderData> cache = CACHE;
            ClassLoaderData data = cache.get(loader);
            // dubbo check
            if (data == null) {
                synchronized (AbstractClassGenerator.class) {
                    cache = CACHE;
                    data = cache.get(loader);
                    if (data == null) {
                        Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
                        //这里创建ClassLoaderData对象
                        data = new ClassLoaderData(loader);
                        newCache.put(loader, data);
                        CACHE = newCache;
                    }
                }
            }
            this.key = key;
            // 获得代理对象的class字节码
            Object obj = data.get(this, getUseCache())
            //实例化该代理对象
            if (obj instanceof Class) {
                return firstInstance((Class) obj);
            }
            return nextInstance(obj);
        }
    }
    

    生成代理类的class字节码并实例化对象

    下面看看创建ClassLoaderData的构造方方法

    private static final Function<AbstractClassGenerator, Object> GET_KEY = new Function<AbstractClassGenerator, Object>() {
        public Object apply(AbstractClassGenerator gen) {
            return gen.key;
        }
    };
    // 创建
    public ClassLoaderData(ClassLoader classLoader) {
        if (classLoader == null) {
            throw new IllegalArgumentException("classLoader == null is not yet supported");
        }
        this.classLoader = new WeakReference<ClassLoader>(classLoader);
        Function<AbstractClassGenerator, Object> load =
            new Function<AbstractClassGenerator, Object>() {
            public Object apply(AbstractClassGenerator gen) {
                Class klass = gen.generate(ClassLoaderData.this);
                return gen.wrapCachedClass(klass);
            }
        };
        //这里GET_KEY是上面的Function接口对象
        generatedClasses = new LoadingCache<AbstractClassGenerator, Object, Object>(GET_KEY, load);
    }
    

    创建了一个LoadingCache对象(generatedClasses), 我们发现同事传入了两个function进去(GET_KEY,load)

    下面继续get方法

    // 这里默认是使用了缓存
    public Object get(AbstractClassGenerator gen, boolean useCache) {
        if (!useCache) {
            return gen.generate(ClassLoaderData.this);
        } else {
            Object cachedValue = generatedClasses.get(gen);
            return gen.unwrapCachedValue(cachedValue);
        }
    }
    

    System.getProperty("cglib.useCache", false); //可以通过这个修改cglib是否使用缓存, 建议默认

    LoadingCache

    调用LoadingCache的get方法

    keyMapper就是 GET_KEY,构造方法传入的, map是缓存集合,先校验是否缓存名中,如果没有则创建一个。

    public V get(K key) {
        final KK cacheKey = keyMapper.apply(key);
        Object v = map.get(cacheKey);
        if (v != null && !(v instanceof FutureTask)) {
            return (V) v;
        }
        return createEntry(key, cacheKey, v);
    }
    
    protected V createEntry(final K key, KK cacheKey, Object v) {
            FutureTask<V> task;
            boolean creator = false;
            if (v != null) {
                // Another thread is already loading an instance
                task = (FutureTask<V>) v;
            } else {
                task = new FutureTask<V>(new Callable<V>() {
                    public V call() throws Exception {
                        //调用load Function接口的方法
                        return loader.apply(key);
                    }
                });
                Object prevTask = map.putIfAbsent(cacheKey, task);
                if (prevTask == null) {
                    // creator does the load
                    creator = true;
                    task.run();
                } else if (prevTask instanceof FutureTask) {
                    task = (FutureTask<V>) prevTask;
                } else {
                    return (V) prevTask;
                }
            }
    
            V result;
            try {
                //一直阻塞,直到得到结果
                result = task.get();
            } catch (InterruptedException e) {
                throw new IllegalStateException("Interrupted while loading cache item", e);
            } catch (ExecutionException e) {
                Throwable cause = e.getCause();
                if (cause instanceof RuntimeException) {
                    throw ((RuntimeException) cause);
                }
                throw new IllegalStateException("Unable to load cache item", cause);
            }
        	//加入缓存
            if (creator) {
                map.put(cacheKey, result);
            }
            return result;
        }
    

    上面的kk是EnhancerKey调用newInstance生成的,源码在Enhancer.createHelper

    让我门再次来到ClassLoaderData的构造方法

    Function<AbstractClassGenerator, Object> load =
        new Function<AbstractClassGenerator, Object>() {
        public Object apply(AbstractClassGenerator gen) {
            Class klass = gen.generate(ClassLoaderData.this);
            return gen.wrapCachedClass(klass);
        }
    };
    

    只差门前一脚了

    AbstractClassGenerator

    protected Class generate(ClassLoaderData data) {
            Class gen;
            Object save = CURRENT.get();
            CURRENT.set(this);
            try {
                // 获取类加载器
                ClassLoader classLoader = data.getClassLoader();
                if (classLoader == null) {
                    throw new IllegalStateException("...);
                }
                synchronized (classLoader) {
                  //代理类的类名  父类名(或者接口名)+$$+Enhancer+ByCGLIB+$$+key的hashCode()
                  // 例如: HelloSub$$EnhancerByCGLIB$$dcdd7f9
                  String name = generateClassName(data.getUniqueNamePredicate());              
                  data.reserveName(name);
                  this.setClassName(name);
                }
                //如果设置,CGLIB将在生成之前尝试从指定的ClassLoader加载类。由于不保证生成的类名称是唯一的,因此默认值为false。
                //因此我们忽略
                if (attemptLoad) {
                    try {
                        gen = classLoader.loadClass(getClassName());
                        return gen;
                    } catch (ClassNotFoundException e) {
                    }
                }
                //生成代理类的字节码  这里会生成字节码文件保存到本地(如果需要)
                byte[] b = strategy.generate(this);
                //类名
                String className = ClassNameReader.getClassName(new ClassReader(b));
                ProtectionDomain protectionDomain = getProtectionDomain();
                // 生成动态类的class文件
                synchronized (classLoader) { // just in case  以防止万一
                    if (protectionDomain == null) {
                        gen = ReflectUtils.defineClass(className, b, classLoader);
                    } else {
                        gen = ReflectUtils.defineClass(className, b, classLoader, protectionDomain);
                    }
                }
                return gen;
            } finally {
                CURRENT.set(save);
            }
        }
    

    最后就是生成字节码了

    Enhancer的generateClass方法中。

    https://www.cnblogs.com/zuidongfeng/p/8735241.html

    参考文章

  • 相关阅读:
    你的系统需要做系统集成测试么?
    测试驱动 ASP.NET MVC 和构建可测试 ASP.NET MVC 应用程序
    RikMigrations 或 Migrator.NET 进行自动化的数据库升级
    单元测试
    C#反射
    J2EE--Struts2基础开发
    Dynamics CRM 客户端的插件调试
    于快速创建 IEqualityComparer<T> 实例的类 Equality<T>
    ToolBox Analysis & Design
    实现$.fn.extend 和$.extend函数
  • 原文地址:https://www.cnblogs.com/qiaozhuangshi/p/11185089.html
Copyright © 2011-2022 走看看