zoukankan      html  css  js  c++  java
  • jdk代理和cglib代理源代码之我见

    以前值是读过一遍jdk和cglib的代理,时间长了,都忘记入口在哪里了,值是记得其中的一些重点了,今天写一篇博客,当作是笔记。和以前一样,关键代码,我会用红色标记出来。

    首先,先列出我的jdk代理对象和测试代码:

    package com.example.gof.proxy;
    
    /**
     * 买车接口
     */
    public interface BuyCard {
        void buycard();
    }
    package com.example.gof.proxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    public class BuyCardDynamicProxy implements InvocationHandler {
        private Object object;
    
        public BuyCardDynamicProxy(final Object object) {
            this.object = object;
        }
    
        /**
         *  代理回调的方法,反编译后可以看到调用的是这段代码:super.h.invoke(this, m3, null);
         *  m3 = Class.forName("com.example.gof.proxy.BuyCard").getMethod("buycard", new Class[0]);
         * @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 res= method.invoke(object,args);
            System.out.println("动态代理,洗刷刷");
            return res;
        }
    }
    package com.example.gof.proxy;
    
    import com.example.gof.proxy.impl.BuyCardImpl;
    import sun.misc.ProxyGenerator;
    
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.lang.reflect.Proxy;
    
    public class TestBuyCardDynamicProxy {
        public static void main(String[] args) throws IOException {
            BuyCard buyCard = new BuyCardImpl();
            BuyCard buyCardProxy = (BuyCard) Proxy.newProxyInstance(BuyCard.class.getClassLoader(),
                    new Class[]{BuyCard.class}, new BuyCardDynamicProxy(buyCard));
            buyCardProxy.buycard();
    //下面这段是用于生成字节码分析的
    // byte[] classFile= ProxyGenerator.generateProxyClass("$Proxy", new Class<?>[]{BuyCard.class}); // File file=new File("D:\java\GoF\src\main\java\com\example\gof\proxy/$Proxy.class"); // FileOutputStream os=new FileOutputStream(file); // os.write(classFile); // os.flush(); // os.close(); } }

    跟踪进去,查看 Proxy.newProxyInstance(BuyCard.class.getClassLoader() 这个方法,代码如下:

     /**
         * Returns an instance of a proxy class for the specified interfaces
         * that dispatches method invocations to the specified invocation
         * handler.
         * 返回一个代理类实例,用于调用响应的接口方法
         * <p>{@code Proxy.newProxyInstance} throws
         * {@code IllegalArgumentException} for the same reasons that
         * {@code Proxy.getProxyClass} does.
         *
         * @param   loader the class loader to define the proxy class
         * @param   interfaces the list of interfaces for the proxy class
         *          to implement
         * @param   h the invocation handler to dispatch method invocations to
         * @return  a proxy instance with the specified invocation handler of a
         *          proxy class that is defined by the specified class loader
         *          and that implements the specified interfaces
         * @throws  IllegalArgumentException if any of the restrictions on the
         *          parameters that may be passed to {@code getProxyClass}
         *          are violated
         * @throws  SecurityException if a security manager, <em>s</em>, is present
         *          and any of the following conditions is met:
         *          <ul>
         *          <li> the given {@code loader} is {@code null} and
         *               the caller's class loader is not {@code null} and the
         *               invocation of {@link SecurityManager#checkPermission
         *               s.checkPermission} with
         *               {@code RuntimePermission("getClassLoader")} permission
         *               denies access;</li>
         *          <li> for each proxy interface, {@code intf},
         *               the caller's class loader is not the same as or an
         *               ancestor of the class loader for {@code intf} and
         *               invocation of {@link SecurityManager#checkPackageAccess
         *               s.checkPackageAccess()} denies access to {@code intf};</li>
         *          <li> any of the given proxy interfaces is non-public and the
         *               caller class is not in the same {@linkplain Package runtime package}
         *               as the non-public interface and the invocation of
         *               {@link SecurityManager#checkPermission s.checkPermission} with
         *               {@code ReflectPermission("newProxyInPackage.{package name}")}
         *               permission denies access.</li>
         *          </ul>
         * @throws  NullPointerException if the {@code interfaces} array
         *          argument or any of its elements are {@code null}, or
         *          if the invocation handler, {@code h}, is
         *          {@code null}
         */
        @CallerSensitive
        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) {
                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; } }); } return cons.newInstance(new Object[]{h}); } catch (IllegalAccessException|InstantiationException e) { throw new InternalError(e.toString(), e); } catch (InvocationTargetException e) { Throwable t = e.getCause(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new InternalError(t.toString(), t); } } catch (NoSuchMethodException e) { throw new InternalError(e.toString(), e); } }
        /**
         * a cache of proxy classes
         */
        private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
            proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()); 
    
        /**
         * Generate a proxy class.  Must call the checkProxyAccess method
         * to perform permission checks before calling this.
         */
        private static Class<?> getProxyClass0(ClassLoader loader,
                                               Class<?>... interfaces) {
            if (interfaces.length > 65535) {
                throw new IllegalArgumentException("interface limit exceeded");
            }
    
            // If the proxy class defined by the given loader implementing
            // the given interfaces exists, this will simply return the cached copy;
            // otherwise, it will create the proxy class via the ProxyClassFactory
            return proxyClassCache.get(loader, interfaces);
        }

    上面这个代码当初看了很久,怎么到proxyClassCache.get(loader, interfaces); 就已经生成了代理对象呢。后来百度下,才知道proxyClassCache9 这个对象调用构造参数时候,就生成了二进制文件,核心代码也在这里。继续往下看

        /**
         * A factory function that generates, defines and returns the proxy class given
         * the ClassLoader and array of interfaces.
    * 一个工厂函数,它生成、定义并返回给定类加载器和接口数组的代理类。
    */ private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> { // prefix for all proxy class names private static final String proxyClassNamePrefix = "$Proxy"; // next number to use for generation of unique proxy class names private static final AtomicLong nextUniqueNumber = new AtomicLong(); @Override public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); for (Class<?> intf : interfaces) { /* * Verify that the class loader resolves the name of this * interface to the same Class object. */ 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"); } /* * Verify that the Class object actually represents an * interface. */ if (!interfaceClass.isInterface()) { throw new IllegalArgumentException( interfaceClass.getName() + " is not an interface"); } /* * Verify that this interface is not a duplicate. */ 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"); } } } if (proxyPkg == null) { // if no non-public proxy interfaces, use com.sun.proxy package proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; } /* * Choose a name for the proxy class to generate. */ long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; /* * Generate the specified proxy class.
    * 生成代理对象
    */ byte[] proxyClassFile = ProxyGenerator.generateProxyClass( //生成二进制字节码 proxyName, interfaces, accessFlags); try {
    //加载到jvm里,返回代理对象
    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()); } } }

    继续跟进生成二进制字节码的方法:ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags)

    public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
            ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
            final byte[] var4 = var3.generateClassFile();
            if (saveGeneratedFiles) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        try {
                            int var1 = var0.lastIndexOf(46);
                            Path var2;
                            if (var1 > 0) {
                                Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
                                Files.createDirectories(var3);
                                var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
                            } else {
                                var2 = Paths.get(var0 + ".class");
                            }
    
                            Files.write(var2, var4, new OpenOption[0]);
                            return null;
                        } catch (IOException var4x) {
                            throw new InternalError("I/O exception saving generated file: " + var4x);
                        }
                    }
                });
            }
    
            return var4;
        }

    由上面可以看见,核心代码,就是生成了二进制字节码文件Files.write(var2, var4, new OpenOption[0]);

    为什么我们看不见呢,那是因为被删除了,可以手动生成这个二进制文件(.class后缀的),也就是上面的那段测试代码的例子。

    下面是二进制字节码的文件内容:

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    import com.example.gof.proxy.BuyCard;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    
    public final class $Proxy extends Proxy implements BuyCard {
        private static Method m1;
        private static Method m2;
        private static Method m3;
        private static Method m0;
    
        public $Proxy(InvocationHandler var1) throws  {
            super(var1);
        }
    
        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 (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        public final void buycard() throws  {
            try {
                super.h.invoke(this, m3, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        public final int hashCode() throws  {
            try {
                return (Integer)super.h.invoke(this, m0, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
                m2 = Class.forName("java.lang.Object").getMethod("toString");
                m3 = Class.forName("com.example.gof.proxy.BuyCard").getMethod("buycard");
                m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch (NoSuchMethodException var2) {
                throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
                throw new NoClassDefFoundError(var3.getMessage());
            }
        }
    }

    从上面可以看到,除了自定义的buycard() 方法外,还有toString()、equals()、hashCode() 三个方法。这三个方法是Object 的方法,每个接口、类都会从Object继承这三个方法。当调用buycard()的时候,可以看到调用的是super.h.invoke(xxx) 这样调用的。这里的h,就是我们自定义的,继承了InvocationHandler的类:BuyCardDynamicProxy。

    至于invoke方式是怎么调用的,在另外一篇博客https://www.cnblogs.com/drafire/p/9637349.html 继续解析

     ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    下面看下cglib的代码解读,依然是先贴出测试代码

    public class BuyCardCglibProxy implements MethodInterceptor {
        private Object target;
    
        public Object getInstance(final Object target) {
            this.target = target;
            Enhancer enhancer=new Enhancer();
            enhancer.setSuperclass(target.getClass());
            enhancer.setCallback(this);
            return enhancer.create();
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("cglib动态代理前");
            //这里使用的是methodProxy.invokeSuper,而不是method.invoke
            //代理类调用父类的方法
            Object res= methodProxy.invokeSuper(o,objects);
            System.out.println("cglib动态代理后");
            return res;
        }
    }
    package com.example.gof.proxy;
    
    import com.example.gof.proxy.impl.BuyCardImpl;
    import org.springframework.cglib.core.DebuggingClassWriter;
    
    public class TestBuyCardCglibProxy {
        public static void main(String[] args) {
            System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, TestBuyCardCglibProxy.class.getClass().getResource("/").getPath() );
            BuyCard buyCard=new BuyCardImpl();
            BuyCardCglibProxy proxy=new BuyCardCglibProxy();
            BuyCardImpl impl= (BuyCardImpl)proxy.getInstance(buyCard);
            impl.buycard();
        }
    }

    关键代码在:enhancer.create() 这个方法,跟踪进去,代码如下:

    public Object create() {
            this.classOnly = false;
            this.argumentTypes = null;
            return this.createHelper();
        }
     private Object createHelper() {
            this.preValidate();
            Object key = KEY_FACTORY.newInstance(this.superclass != null ? this.superclass.getName() : null, ReflectUtils.getNames(this.interfaces), this.filter == ALL_ZERO ? null : new WeakCacheKey(this.filter), this.callbackTypes, this.useFactory, this.interceptDuringConstruction, this.serialVersionUID);
            this.currentKey = key;
            Object result = super.create(key);
            return result;
        }
    protected Object create(Object key) {
            try {
                ClassLoader loader = this.getClassLoader();   //获取加载器
                Map<ClassLoader, AbstractClassGenerator.ClassLoaderData> cache = CACHE;
    //从缓存中读取 AbstractClassGenerator.ClassLoaderData data
    = (AbstractClassGenerator.ClassLoaderData)cache.get(loader); if (data == null) { //如果缓存没有对应的数据 Class var5 = AbstractClassGenerator.class; synchronized(AbstractClassGenerator.class) { cache = CACHE; data = (AbstractClassGenerator.ClassLoaderData)cache.get(loader); if (data == null) { Map<ClassLoader, AbstractClassGenerator.ClassLoaderData> newCache = new WeakHashMap(cache); data = new AbstractClassGenerator.ClassLoaderData(loader); //生成二进制字节码 newCache.put(loader, data); CACHE = newCache; } } } this.key = key; Object obj = data.get(this, this.getUseCache());
    //生成代理对象
    return obj instanceof Class ? this.firstInstance((Class)obj) : this.nextInstance(obj); } catch (RuntimeException var9) { throw var9; } catch (Error var10) { throw var10; } catch (Exception var11) { throw new CodeGenerationException(var11); } }
     public ClassLoaderData(ClassLoader classLoader) {
                if (classLoader == null) {
                    throw new IllegalArgumentException("classLoader == null is not yet supported");
                } else {
                    this.classLoader = new WeakReference(classLoader);
                    Function<AbstractClassGenerator, Object> load = new Function<AbstractClassGenerator, Object>() {
                        public Object apply(AbstractClassGenerator gen) {
                            Class klass = gen.generate(ClassLoaderData.this);   //核心代码
                            return gen.wrapCachedClass(klass);
                        }
                    };
                    this.generatedClasses = new LoadingCache(GET_KEY, load);
                }
            }
    protected Class generate(AbstractClassGenerator.ClassLoaderData data) {
            Object save = CURRENT.get();
            CURRENT.set(this);
    
            Class var8;
            try {
                ClassLoader classLoader = data.getClassLoader();
                if (classLoader == null) {
                    throw new IllegalStateException("ClassLoader is null while trying to define class " + this.getClassName() + ". It seems that the loader has been expired from a weak reference somehow. Please file an issue at cglib's issue tracker.");
                }
    
                String className;
                synchronized(classLoader) {
                    className = this.generateClassName(data.getUniqueNamePredicate());
                    data.reserveName(className);
                    this.setClassName(className);
                }
    
                Class gen;
                if (this.attemptLoad) {
                    try {
                        gen = classLoader.loadClass(this.getClassName());
                        Class var25 = gen;
                        return var25;
                    } catch (ClassNotFoundException var20) {
                        ;
                    }
                }
    
                byte[] b = this.strategy.generate(this);   //核心代码
                className = ClassNameReader.getClassName(new ClassReader(b));
                ProtectionDomain protectionDomain = this.getProtectionDomain();
                synchronized(classLoader) {
                    if (protectionDomain == null) {
                        gen = ReflectUtils.defineClass(className, b, classLoader);
                    } else {
                        gen = ReflectUtils.defineClass(className, b, classLoader, protectionDomain);
                    }
                }
    
                var8 = gen;
            } catch (RuntimeException var21) {
                throw var21;
            } catch (Error var22) {
                throw var22;
            } catch (Exception var23) {
                throw new CodeGenerationException(var23);
            } finally {
                CURRENT.set(save);
            }
    
            return var8;
        }
    //orgspringframeworkspring-core5.0.8.RELEASEspring-core-5.0.8.RELEASE.jar!orgspringframeworkcglibcoreDefaultGeneratorStrategy.class
    public
    byte[] generate(ClassGenerator cg) throws Exception { DebuggingClassWriter cw = this.getClassVisitor(); //从这里的源代码,可以看出,底层是使用asm包的 this.transform(cg).generateClass(cw); return this.transform(cw.toByteArray()); }
    //orgspringframeworkspring-core5.0.8.RELEASEspring-core-5.0.8.RELEASE.jar!orgspringframeworkcglibcoreKeyFactory.class
    
    public void generateClass(ClassVisitor v) {
                ClassEmitter ce = new ClassEmitter(v);
                Method newInstance = ReflectUtils.findNewInstance(this.keyInterface);
                if (!newInstance.getReturnType().equals(Object.class)) {
                    throw new IllegalArgumentException("newInstance method must return Object");
                } else {
                    Type[] parameterTypes = TypeUtils.getTypes(newInstance.getParameterTypes());
                    ce.begin_class(46, 1, this.getClassName(), KeyFactory.KEY_FACTORY, new Type[]{Type.getType(this.keyInterface)}, "<generated>");
                    EmitUtils.null_constructor(ce);
                    EmitUtils.factory_method(ce, ReflectUtils.getSignature(newInstance));
                    int seed = 0;
                    CodeEmitter e = ce.begin_method(1, TypeUtils.parseConstructor(parameterTypes), (Type[])null);
                    e.load_this();
                    e.super_invoke_constructor();
                    e.load_this();
                    List<FieldTypeCustomizer> fieldTypeCustomizers = this.getCustomizers(FieldTypeCustomizer.class);
    
                    int i;
                    for(i = 0; i < parameterTypes.length; ++i) {
                        Type parameterType = parameterTypes[i];
                        Type fieldType = parameterType;
    
                        Iterator var11;
                        FieldTypeCustomizer customizer;
                        for(var11 = fieldTypeCustomizers.iterator(); var11.hasNext(); fieldType = customizer.getOutType(i, fieldType)) {
                            customizer = (FieldTypeCustomizer)var11.next();
                        }
    
                        seed += fieldType.hashCode();
                        ce.declare_field(18, this.getFieldName(i), fieldType, (Object)null);
                        e.dup();
                        e.load_arg(i);
                        var11 = fieldTypeCustomizers.iterator();
    
                        while(var11.hasNext()) {
                            customizer = (FieldTypeCustomizer)var11.next();
                            customizer.customize(e, i, parameterType);
                        }
    
                        e.putfield(this.getFieldName(i));
                    }
    
                    e.return_value();
                    e.end_method();
                    e = ce.begin_method(1, KeyFactory.HASH_CODE, (Type[])null);
                    i = this.constant != 0 ? this.constant : KeyFactory.PRIMES[Math.abs(seed) % KeyFactory.PRIMES.length];
                    int hm = this.multiplier != 0 ? this.multiplier : KeyFactory.PRIMES[Math.abs(seed * 13) % KeyFactory.PRIMES.length];
                    e.push(i);
    
                    for(int i = 0; i < parameterTypes.length; ++i) {
                        e.load_this();
                        e.getfield(this.getFieldName(i));
                        EmitUtils.hash_code(e, parameterTypes[i], hm, this.customizers);
                    }
    
                    e.return_value();
                    e.end_method();
                    e = ce.begin_method(1, KeyFactory.EQUALS, (Type[])null);
                    Label fail = e.make_label();
                    e.load_arg(0);
                    e.instance_of_this();
                    e.if_jump(153, fail);
    
                    int i;
                    for(i = 0; i < parameterTypes.length; ++i) {
                        e.load_this();
                        e.getfield(this.getFieldName(i));
                        e.load_arg(0);
                        e.checkcast_this();
                        e.getfield(this.getFieldName(i));
                        EmitUtils.not_equals(e, parameterTypes[i], fail, this.customizers);
                    }
    
                    e.push(1);
                    e.return_value();
                    e.mark(fail);
                    e.push(0);
                    e.return_value();
                    e.end_method();
                    e = ce.begin_method(1, KeyFactory.TO_STRING, (Type[])null);
                    e.new_instance(Constants.TYPE_STRING_BUFFER);
                    e.dup();
                    e.invoke_constructor(Constants.TYPE_STRING_BUFFER);
    
                    for(i = 0; i < parameterTypes.length; ++i) {
                        if (i > 0) {
                            e.push(", ");
                            e.invoke_virtual(Constants.TYPE_STRING_BUFFER, KeyFactory.APPEND_STRING);
                        }
    
                        e.load_this();
                        e.getfield(this.getFieldName(i));
                        EmitUtils.append_string(e, parameterTypes[i], EmitUtils.DEFAULT_DELIMITERS, this.customizers);
                    }
    
                    e.invoke_virtual(Constants.TYPE_STRING_BUFFER, KeyFactory.TO_STRING);
                    e.return_value();
                    e.end_method();
                    ce.end_class();
                }
            }
  • 相关阅读:
    关于Velocity加减法等四则运算的迷思
    [有明信息]科目导向,精耕细作 ——浅谈房地产开发成本管理
    The Building Blocks-Enterprise Applications Part 2- Information Management and Business Analytics
    maven项目建立pom.xml报无法解析org.apache.maven.plugins:maven-resources-plugin:2.4.3
    编程离不开生活
    原声JS瀑布流加延迟载入
    uva 11722
    Android自定义控件View(三)组合控件
    Android自定义控件View(二)继承控件
    Android自定义控件View(一)
  • 原文地址:https://www.cnblogs.com/drafire/p/10409424.html
Copyright © 2011-2022 走看看