电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

您的位置:首頁技術文章
文章詳情頁

JAVA深入探究之Method的Invoke方法

瀏覽:26日期:2022-08-18 15:40:40
前言

在寫代碼的時候,發現從父類class通過getDeclaredMethod獲取的Method可以調用子類的對象,而子類改寫了這個方法,從子類class通過getDeclaredMethod也能獲取到Method,這時去調用父類的對象也會報錯。雖然這是很符合多態的現象,也符合java的動態綁定規范,但還是想弄懂java是如何實現的,就學習了下Method的源代碼。

Method的invoke方法

1.先檢查 AccessibleObject的override屬性是否為true。

AccessibleObject是Method,Field,Constructor的父類,override屬性默認為false,可調用setAccessible方法改變,如果設置為true,則表示可以忽略訪問權限的限制,直接調用。

2.如果不是ture,則要進行訪問權限檢測。用Reflection的quickCheckMemberAccess方法先檢查是不是public的,如果不是再用Reflection.getCallerClass(1)方法獲

得到調用這個方法的Class,然后做是否有權限訪問的校驗,校驗之后緩存一次,以便下次如果還是這個類來調用就不用去做校驗了,直接用上次的結果,(很奇怪用這種方式緩存,因為這種方式如果下次換個類來調用的話,就不用會緩存了,而再驗證一遍,把這次的結果做為緩存,但上一次的緩存結果就被沖掉了。這是一個很簡單的緩沖機制,只適用于一個類的重復調用)。

3.調用MethodAccessor的invoke方法。每個Method對象包含一個root對象,root對象里持有一個MethodAccessor對象。我們獲得的Method獨享相當于一個root對象的鏡像,所有這類Method共享root里的MethodAccessor對象,(這個對象由ReflectionFactory方法生成,ReflectionFactory對象在Method類中是static final的由native方法實例化)。

ReflectionFactory生成MethodAccessor:如果noInflation的屬性為true則直接返回MethodAccessorGenerator創建的一個MethodAccessor。

否則返回DelegatingMethodAccessorImpl,并將他與一個NativeMethodAccessorImpl互相引用。但DelegatingMethodAccessorImpl執行invoke方法的時候又委托給NativeMethodAccessorImpl了。

再一步深入

4.NativeMethodAccessorImpl的invkoe方法:

調用natiave方法invoke0執行方法調用.

注意這里有一個計數器numInvocations,每調用一次方法+1,當比 ReflectionFactory.inflationThreshold(15)大的時候,用MethodAccessorGenerator創建一個MethodAccessor,并把之前的DelegatingMethodAccessorImpl引用替換為現在新創建的。下一次DelegatingMethodAccessorImpl就不會再交給NativeMethodAccessorImpl執行了,而是交給新生成的java字節碼的MethodAccessor。

MethodAccessorGenerator使用了asm字節碼動態加載技術,暫不深入研究。

總結 一個方法可以生成多個Method對象,但只有一個root對象,主要用于持有一個MethodAccessor對象,這個對象也可以認為一個方法只有一個,相當于是static的。因為Method的invoke是交給MethodAccessor執行的,所以我所想要知道的答案在MethodAccessor的invoke中,深入MethodAccessor:

MethodAccessor

public class A {public void foo(String name) {System.out.println('Hello, ' + name);}}

可以編寫另外一個類來反射調用A上的方法:

import java.lang.reflect.Method;public class TestClassLoad {public static void main(String[] args) throws Exception {Class<?> clz = Class.forName('A');Object o = clz.newInstance();Method m = clz.getMethod('foo', String.class);for (int i = 0; i < 16; i++) {m.invoke(o, Integer.toString(i));}}}

注意到TestClassLoad類上不會有對類A的符號依賴——也就是說在加載并初始化TestClassLoad類時不需要關心類A的存在與否,而是等到main()方法執行到調用Class.forName()時才試圖對類A做動態加載;這里用的是一個參數版的forName(),也就是使用當前方法所在類的ClassLoader來加載,并且初始化新加載的類?!冒蛇@個細節跟主題沒啥關系。

回到主題。這次我的測試環境是Sun的JDK 1.6.0 update 13 build 03。編譯上述代碼,并在執行TestClassLoad時加入-XX:+TraceClassLoading參數(或者-verbose:class或者直接-verbose都行),如下:

控制臺命令

java -XX:+TraTestClassLoad ceClassLoading

可以看到輸出了一大堆log,把其中相關的部分截取出來如下:

[Loaded TestClassLoad from file:/D:/temp_code/test_java_classload/][Loaded A from file:/D:/temp_code/test_java_classload/][Loaded sun.reflect.NativeMethodAccessorImpl from shared objects file][Loaded sun.reflect.DelegatingMethodAccessorImpl from shared objects file]Hello, 0Hello, 1Hello, 2Hello, 3Hello, 4Hello, 5Hello, 6Hello, 7Hello, 8Hello, 9Hello, 10Hello, 11Hello, 12Hello, 13Hello, 14[Loaded sun.reflect.ClassFileConstants from shared objects file][Loaded sun.reflect.AccessorGenerator from shared objects file][Loaded sun.reflect.MethodAccessorGenerator from shared objects file][Loaded sun.reflect.ByteVectorFactory from shared objects file][Loaded sun.reflect.ByteVector from shared objects file][Loaded sun.reflect.ByteVectorImpl from shared objects file][Loaded sun.reflect.ClassFileAssembler from shared objects file][Loaded sun.reflect.UTF8 from shared objects file][Loaded java.lang.Void from shared objects file][Loaded sun.reflect.Label from shared objects file][Loaded sun.reflect.Label$PatchInfo from shared objects file][Loaded java.util.AbstractList$Itr from shared objects file][Loaded sun.reflect.MethodAccessorGenerator$1 from shared objects file][Loaded sun.reflect.ClassDefiner from shared objects file][Loaded sun.reflect.ClassDefiner$1 from shared objects file][Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]Hello, 15

可以看到前15次反射調用A.foo()方法并沒有什么稀奇的地方,但在第16次反射調用時似乎有什么東西被觸發了,導致JVM新加載了一堆類,其中就包括[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]這么一行。這是哪里來的呢?

先來看看JDK里Method.invoke()是怎么實現的。

java.lang.reflect.Method:

public final class Method extends AccessibleObject implements GenericDeclaration, Member { // ... private volatile MethodAccessor methodAccessor; // For sharing of MethodAccessors. This branching structure is // currently only two levels deep (i.e., one root Method and // potentially many Method objects pointing to it.) private Method root; // ... public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class caller = Reflection.getCallerClass(1); Class targetClass = ((obj == null || !Modifier.isProtected(modifiers)) ? clazz : obj.getClass()); boolean cached; synchronized (this) { cached = (securityCheckCache == caller) && (securityCheckTargetClassCache == targetClass); } if (!cached) { Reflection.ensureMemberAccess(caller, clazz, obj, modifiers); synchronized (this) { securityCheckCache = caller; securityCheckTargetClassCache = targetClass; } } } } if (methodAccessor == null) acquireMethodAccessor(); return methodAccessor.invoke(obj, args); } // NOTE that there is no synchronization used here. It is correct // (though not efficient) to generate more than one MethodAccessor // for a given Method. However, avoiding synchronization will // probably make the implementation more scalable. private void acquireMethodAccessor() { // First check to see if one has been created yet, and take it // if so MethodAccessor tmp = null; if (root != null) tmp = root.getMethodAccessor(); if (tmp != null) { methodAccessor = tmp; return; } // Otherwise fabricate one and propagate it up to the root tmp = reflectionFactory.newMethodAccessor(this); setMethodAccessor(tmp); } // ...}

可以看到Method.invoke()實際上并不是自己實現的反射調用邏輯,而是委托給sun.reflect.MethodAccessor來處理。 每個實際的Java方法只有一個對應的Method對象作為root,。這個root是不會暴露給用戶的,而是每次在通過反射獲取Method對象時新創建Method對象把root包裝起來再給用戶。在第一次調用一個實際Java方法對應得Method對象的invoke()方法之前,實現調用邏輯的MethodAccessor對象還沒創建;等第一次調用時才新創建MethodAccessor并更新給root,然后調用MethodAccessor.invoke()真正完成反射調用。

那么MethodAccessor是啥呢?

sun.reflect.MethodAccessor:

public interface MethodAccessor { /** Matches specification in {@link java.lang.reflect.Method} */ public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException;}

可以看到它只是一個單方法接口,其invoke()方法與Method.invoke()的對應。

創建MethodAccessor實例的是ReflectionFactory。

sun.reflect.ReflectionFactory:

public class ReflectionFactory { private static boolean initted = false; // ... // // 'Inflation' mechanism. Loading bytecodes to implement // Method.invoke() and Constructor.newInstance() currently costs // 3-4x more than an invocation via native code for the first // invocation (though subsequent invocations have been benchmarked // to be over 20x faster). Unfortunately this cost increases // startup time for certain applications that use reflection // intensively (but only once per class) to bootstrap themselves. // To avoid this penalty we reuse the existing JVM entry points // for the first few invocations of Methods and Constructors and // then switch to the bytecode-based implementations. // // Package-private to be accessible to NativeMethodAccessorImpl // and NativeConstructorAccessorImpl private static boolean noInflation = false; private static int inflationThreshold = 15; // ... /** We have to defer full initialization of this class until after the static initializer is run since java.lang.reflect.Method’s static initializer (more properly, that for java.lang.reflect.AccessibleObject) causes this class’s to be run, before the system properties are set up. */ private static void checkInitted() { if (initted) return; AccessController.doPrivileged(new PrivilegedAction() { public Object run() { // Tests to ensure the system properties table is fully // initialized. This is needed because reflection code is // called very early in the initialization process (before // command-line arguments have been parsed and therefore // these user-settable properties installed.) We assume that // if System.out is non-null then the System class has been // fully initialized and that the bulk of the startup code // has been run. if (System.out == null) { // java.lang.System not yet fully initialized return null; } String val = System.getProperty('sun.reflect.noInflation'); if (val != null && val.equals('true')) { noInflation = true; } val = System.getProperty('sun.reflect.inflationThreshold'); if (val != null) { try { inflationThreshold = Integer.parseInt(val); } catch (NumberFormatException e) { throw (RuntimeException) new RuntimeException('Unable to parse property sun.reflect.inflationThreshold'). initCause(e); } } initted = true; return null; } }); } // ... public MethodAccessor newMethodAccessor(Method method) { checkInitted(); if (noInflation) { return new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); } else { NativeMethodAccessorImpl acc = new NativeMethodAccessorImpl(method); DelegatingMethodAccessorImpl res = new DelegatingMethodAccessorImpl(acc); acc.setParent(res); return res; } }}

這里就可以看到有趣的地方了。如注釋所述,實際的MethodAccessor實現有兩個版本,一個是Java實現的,另一個是native code實現的。Java實現的版本在初始化時需要較多時間,但長久來說性能較好;native版本正好相反,啟動時相對較快,但運行時間長了之后速度就比不過Java版了。這是HotSpot的優化方式帶來的性能特性,同時也是許多虛擬機的共同點:跨越native邊界會對優化有阻礙作用,它就像個黑箱一樣讓虛擬機難以分析也將其內聯,于是運行時間長了之后反而是托管版本的代碼更快些。

為了權衡兩個版本的性能,Sun的JDK使用了“inflation”的技巧:讓Java方法在被反射調用時,開頭若干次使用native版,等反射調用次數超過閾值時則生成一個專用的MethodAccessor實現類,生成其中的invoke()方法的字節碼,以后對該Java方法的反射調用就會使用Java版。

Sun的JDK是從1.4系開始采用這種優化的。

PS.可以在啟動命令里加上-Dsun.reflect.noInflation=true,就會RefactionFactory的noInflation屬性就變成true了,這樣不用等到15調用后,程序一開始就會用java版的MethodAccessor了。

上面看到了ReflectionFactory.newMethodAccessor()生產MethodAccessor的邏輯,在“開頭若干次”時用到的DelegatingMethodAccessorImpl代碼如下:

sun.reflect.DelegatingMethodAccessorImpl:

/** Delegates its invocation to another MethodAccessorImpl and can change its delegate at run time. */class DelegatingMethodAccessorImpl extends MethodAccessorImpl { private MethodAccessorImpl delegate; DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) { setDelegate(delegate); } public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { return delegate.invoke(obj, args); } void setDelegate(MethodAccessorImpl delegate) { this.delegate = delegate; }}

這是一個間接層,方便在native與Java版的MethodAccessor之間實現切換。

然后下面就是native版MethodAccessor的Java一側的聲明:

sun.reflect.NativeMethodAccessorImpl:

/** Used only for the first few invocations of a Method; afterward, switches to bytecode-based implementation */class NativeMethodAccessorImpl extends MethodAccessorImpl { private Method method; private DelegatingMethodAccessorImpl parent; private int numInvocations; NativeMethodAccessorImpl(Method method) { this.method = method; } public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { if (++numInvocations > ReflectionFactory.inflationThreshold()) { MethodAccessorImpl acc = (MethodAccessorImpl) new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); parent.setDelegate(acc); } return invoke0(method, obj, args); } void setParent(DelegatingMethodAccessorImpl parent) { this.parent = parent; } private static native Object invoke0(Method m, Object obj, Object[] args);}

每次NativeMethodAccessorImpl.invoke()方法被調用時,都會增加一個調用次數計數器,看超過閾值沒有;一旦超過,則調用MethodAccessorGenerator.generateMethod()來生成Java版的MethodAccessor的實現類,并且改變DelegatingMethodAccessorImpl所引用的MethodAccessor為Java版。后續經由DelegatingMethodAccessorImpl.invoke()調用到的就是Java版的實現了。

注意到關鍵的invoke0()方法是個native方法。它在HotSpot VM里是由JVM_InvokeMethod()函數所支持的:

由C編寫

JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args){ return JVM_InvokeMethod(env, m, obj, args);}

JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0)) JVMWrapper('JVM_InvokeMethod'); Handle method_handle; if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) { method_handle = Handle(THREAD, JNIHandles::resolve(method)); Handle receiver(THREAD, JNIHandles::resolve(obj)); objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0))); oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL); jobject res = JNIHandles::make_local(env, result); if (JvmtiExport::should_post_vm_object_alloc()) { oop ret_type = java_lang_reflect_Method::return_type(method_handle()); assert(ret_type != NULL, 'sanity check: ret_type oop must not be NULL!'); if (java_lang_Class::is_primitive(ret_type)) { // Only for primitive type vm allocates memory for java object. // See box() method. JvmtiExport::post_vm_object_alloc(JavaThread::current(), result); } } return res; } else { THROW_0(vmSymbols::java_lang_StackOverflowError()); }JVM_END

其中的關鍵又是Reflection::invoke_method():

// This would be nicer if, say, java.lang.reflect.Method was a subclass// of java.lang.reflect.Constructoroop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) { oop mirror = java_lang_reflect_Method::clazz(method_mirror); int slot = java_lang_reflect_Method::slot(method_mirror); bool override = java_lang_reflect_Method::override(method_mirror) != 0; objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Method::parameter_types(method_mirror))); oop return_type_mirror = java_lang_reflect_Method::return_type(method_mirror); BasicType rtype; if (java_lang_Class::is_primitive(return_type_mirror)) { rtype = basic_type_mirror_to_basic_type(return_type_mirror, CHECK_NULL); } else { rtype = T_OBJECT; } instanceKlassHandle klass(THREAD, java_lang_Class::as_klassOop(mirror)); methodOop m = klass->method_with_idnum(slot); if (m == NULL) { THROW_MSG_0(vmSymbols::java_lang_InternalError(), 'invoke'); } methodHandle method(THREAD, m); return invoke(klass, method, receiver, override, ptypes, rtype, args, true, THREAD);}

再下去就深入到HotSpot VM的內部了,本文就在這里打住吧。有同學有興趣深究的話以后可以再寫一篇討論native版的實現。

回到Java的一側。MethodAccessorGenerator長啥樣呢?由于代碼太長,這里就不完整貼了,有興趣的可以到OpenJDK 6的Mercurial倉庫看:OpenJDK 6 build 17的MethodAccessorGenerator。它的基本工作就是在內存里生成新的專用Java類,并將其加載。就貼這么一個方法:

private static synchronized String generateName(boolean isConstructor, boolean forSerialization){ if (isConstructor) { if (forSerialization) { int num = ++serializationConstructorSymnum; return 'sun/reflect/GeneratedSerializationConstructorAccessor' + num; } else { int num = ++constructorSymnum; return 'sun/reflect/GeneratedConstructorAccessor' + num; } } else { int num = ++methodSymnum; return 'sun/reflect/GeneratedMethodAccessor' + num; }}

去閱讀源碼的話,可以看到MethodAccessorGenerator是如何一點點把Java版的MethodAccessor實現類生產出來的。也可以看到GeneratedMethodAccessor+數字這種名字是從哪里來的了,就在上面的generateName()方法里。 對本文開頭的例子的A.foo(),生成的Java版MethodAccessor大致如下:

package sun.reflect;public class GeneratedMethodAccessor1 extends MethodAccessorImpl { public GeneratedMethodAccessor1() { super(); } public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { // prepare the target and parameters if (obj == null) throw new NullPointerException(); try { A target = (A) obj; if (args.length != 1) throw new IllegalArgumentException(); String arg0 = (String) args[0]; } catch (ClassCastException e) { throw new IllegalArgumentException(e.toString()); } catch (NullPointerException e) { throw new IllegalArgumentException(e.toString()); } // make the invocation try { target.foo(arg0); } catch (Throwable t) { throw new InvocationTargetException(t); } }}

就反射調用而言,這個invoke()方法非常干凈(然而就“正常調用”而言這額外開銷還是明顯的)。注意到參數數組被拆開了,把每個參數都恢復到原本沒有被Object[]包裝前的樣子,然后對目標方法做正常的invokevirtual調用。由于在生成代碼時已經循環遍歷過參數類型的數組,生成出來的代碼里就不再包含循環了。

至此找到我的答案了,因為MethodAccessor會做強制類型轉換再進行方法調用,但父類強制轉化成子類的的時候就會報錯類型不匹配錯誤了,所以如果變量的引用聲明是父但實際指向的對象是子,那么這種調用也是可以的。

題外話

當該反射調用成為熱點時,它甚至可以被內聯到靠近Method.invoke()的一側,大大降低了反射調用的開銷。而native版的反射調用則無法被有效內聯,因而調用開銷無法隨程序的運行而降低。

雖說Sun的JDK這種實現方式使得反射調用方法成本比以前降低了很多,但Method.invoke()本身要用數組包裝參數;而且每次調用都必須檢查方法的可見性(在Method.invoke()里),也必須檢查每個實際參數與形式參數的類型匹配性(在NativeMethodAccessorImpl.invoke0()里或者生成的Java版MethodAccessor.invoke()里);而且Method.invoke()就像是個獨木橋一樣,各處的反射調用都要擠過去,在調用點上收集到的類型信息就會很亂,影響內聯程序的判斷,使得Method.invoke()自身難以被內聯到調用方。

相比之下JDK7里新的MethodHandler則更有潛力,在其功能完全實現后能達到比普通反射調用方法更高的性能。在使用MethodHandle來做反射調用時,MethodHandle.invoke()的形式參數與返回值類型都是準確的,所以只需要在鏈接方法的時候才需要檢查類型的匹配性,而不必在每次調用時都檢查。而且MethodHandle是不可變值,在創建后其內部狀態就不會再改變了;JVM可以利用這個知識而放心的對它做激進優化,例如將實際的調用目標內聯到做反射調用的一側。

本來Java的安全機制使得不同類之間不是任意信息都可見,但Sun的JDK里開了個口,有一個標記類專門用于開后門:

package sun.reflect;/** <P> MagicAccessorImpl (named for parity with FieldAccessorImpl and others, not because it actually implements an interface) is a marker class in the hierarchy. All subclasses of this class are 'magically' granted access by the VM to otherwise inaccessible fields and methods of other classes. It is used to hold the code for dynamically-generated FieldAccessorImpl and MethodAccessorImpl subclasses. (Use of the word 'unsafe' was avoided in this class’s name to avoid confusion with {@link sun.misc.Unsafe}.) </P> <P> The bug fix for 4486457 also necessitated disabling verification for this class and all subclasses, as opposed to just SerializationConstructorAccessorImpl and subclasses, to avoid having to indicate to the VM which of these dynamically-generated stub classes were known to be able to pass the verifier. </P> <P> Do not change the name of this class without also changing the VM’s code. </P> */class MagicAccessorImpl {}

那個'__JVM_DefineClass__'的來源是這里:

src/share/vm/prims/jvm.cpp

// common code for JVM_DefineClass() and JVM_DefineClassWithSource()// and JVM_DefineClassWithSourceCond()static jclass jvm_define_class_common(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source, jboolean verify, TRAPS) { if (source == NULL) source = '__JVM_DefineClass__';

P.S. log里的'shared objects file',其實就是rt.jar,為什么要這么顯示,Stack OverFlow上有這樣的回答:

This is Class Data Sharing. When running the Sun/Oracle Client HotSpot and sharing enable (either -Xshare:auto which is the default, or -Xshare:on), the classes.jsa file is memory mapped. This file contains a number of classes (listed in the classlist file) in internal representation suitable for the exact configuration of the machine running it. The idea is that the classes can be loaded quickly, getting the the JVM up faster. Soon enough a class not covered will be hit, and rt.jar will need to be opened and classes loaded conventionally as required.

不能很好理解,大概理解就是所有jvm共享,并可以快速加載里面的class.有英文好的朋友可以留言幫助下。

P.S java內聯函數

C++是否為內聯函數由自己決定,Java由編譯器決定。內聯函數就是指函數在被調用的地方直接展開,編譯器在調用時不用像一般函數那樣,參數壓棧,返回時參數出棧以及資源釋放等,這樣提高了程序執行速度。

Java不支持直接聲明為內聯函數的,如果想讓他內聯,則是由編譯器說了算,你只能夠向編譯器提出請求。

final除了不能被override外,還可能實現內聯。如果函數為private,則也可能是內聯的。

總的來說,一般的函數都不會被當做內聯函數,只有聲明了final后,編譯器才會考慮是不是要把你的函數變成內聯函數。

內聯不一定好,當被指定為內聯的方法體很大時,展開的開銷可能就已經超過了普通函數調用調用的時間,引入了內聯反而降低了性能,因為在選擇這個關鍵字應該慎重些,不過,在以后高版本的JVM中,在處理內聯時做出了優化,它會根據方法的規模來確定是否展開調用。

總結

到此這篇關于JAVA深入探究之Method的Invoke方法的文章就介紹到這了,更多相關JAVA Method的Invoke方法內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!

標簽: Java
相關文章:
主站蜘蛛池模板: 胀套-锁紧盘-风电锁紧盘-蛇形联轴器「厂家」-瑞安市宝德隆机械配件有限公司 | 上海租车公司_上海包车_奔驰租赁_上海商务租车_上海谐焕租车 | 全自动实验室洗瓶机,移液管|培养皿|进样瓶清洗机,清洗剂-广州摩特伟希尔机械设备有限责任公司 | 冰雕-冰雪世界-大型冰雕展制作公司-赛北冰雕官网 | 空气能采暖,热泵烘干机,空气源热水机组|设备|厂家,东莞高温热泵_正旭新能源 | 武汉天安盾电子设备有限公司 - 安盾安检,武汉安检门,武汉安检机,武汉金属探测器,武汉测温安检门,武汉X光行李安检机,武汉防爆罐,武汉车底安全检查,武汉液体探测仪,武汉安检防爆设备 | 昆明网络公司|云南网络公司|昆明网站建设公司|昆明网页设计|云南网站制作|新媒体运营公司|APP开发|小程序研发|尽在昆明奥远科技有限公司 | 专业广州网站建设,微信小程序开发,一物一码和NFC应用开发、物联网、外贸商城、定制系统和APP开发【致茂网络】 | 恒温油槽-恒温水槽-低温恒温槽厂家-宁波科麦仪器有限公司 | 玻璃钢型材-玻璃钢风管-玻璃钢管道,生产厂家-[江苏欧升玻璃钢制造有限公司] | Copeland/谷轮压缩机,谷轮半封闭压缩机,谷轮涡旋压缩机,型号规格,技术参数,尺寸图片,价格经销商 CTP磁天平|小电容测量仪|阴阳极极化_双液系沸点测定仪|dsj电渗实验装置-南京桑力电子设备厂 | 威实软件_软件定制开发_OA_OA办公系统_OA系统_办公自动化软件 | 富森高压水枪-柴油驱动-养殖场高压清洗机-山东龙腾环保科技有限公司 | 德国BOSCH电磁阀-德国HERION电磁阀-JOUCOMATIC电磁阀|乾拓百科 | 运动木地板厂家,篮球场木地板品牌,体育场馆木地板安装 - 欧氏运动地板 | 龙门加工中心-数控龙门加工中心厂家价格-山东海特数控机床有限公司_龙门加工中心-数控龙门加工中心厂家价格-山东海特数控机床有限公司 | pbt头梳丝_牙刷丝_尼龙毛刷丝_PP塑料纤维合成毛丝定制厂_广州明旺 | 陕西华春网络科技股份有限公司 | 英语词典_成语词典_日语词典_法语词典_在线词典网 | 上海办公室设计_办公楼,写字楼装修_办公室装修公司-匠御设计 | 知名电动蝶阀,电动球阀,气动蝶阀,气动球阀生产厂家|价格透明-【固菲阀门官网】 | 除湿机|工业除湿机|抽湿器|大型地下室车间仓库吊顶防爆除湿机|抽湿烘干房|新风除湿机|调温/降温除湿机|恒温恒湿机|加湿机-杭州川田电器有限公司 | 电缆桥架生产厂家_槽式/梯式_热镀锌线槽_广东东莞雷正电气 | 必胜高考网_全国高考备考和志愿填报信息平台 | 不锈钢电动球阀_气动高压闸阀_旋塞疏水调节阀_全立阀门-来自温州工业阀门巨头企业 | 三轴曲线机-端子插拔力试验机|华杰仪器 | 聚丙烯酰胺_阴离子_阳离子「用量少」巩义亿腾厂家直销,售后无忧 聚合甘油__盐城市飞龙油脂有限公司 | 【灵硕展览集团】展台展会设计_展览会展台搭建_展览展示设计一站式服务公司 | 密封无忧网 _ 专业的密封产品行业信息网 | 短信通106短信接口验证码接口群发平台_国际短信接口验证码接口群发平台-速度网络有限公司 | 自清洗过滤器-全自动自清洗过反冲洗过滤器 - 中乂(北京)科技有限公司 | 环讯传媒,永康网络公司,永康网站建设,永康小程序开发制作,永康网站制作,武义网页设计,金华地区网站SEO优化推广 - 永康市环讯电子商务有限公司 | 电池挤压试验机-自行车喷淋-车辆碾压试验装置-深圳德迈盛测控设备有限公司 | 新疆散热器,新疆暖气片,新疆电锅炉,光耀暖通公司 | 绿叶|绿叶投资|健康产业_绿叶投资集团有限公司 | 减速机_上海宜嘉减速机| 家德利门业,家居安全门,别墅大门 - 安徽家德利门业有限公司 | 硫化罐_蒸汽硫化罐_大型硫化罐-山东鑫泰鑫智能装备有限公司 | KBX-220倾斜开关|KBW-220P/L跑偏开关|拉绳开关|DHJY-I隔爆打滑开关|溜槽堵塞开关|欠速开关|声光报警器-山东卓信有限公司 | 热处理炉-退火炉-回火炉设备厂家-丹阳市电炉厂有限公司 | 长沙印刷厂-包装印刷-画册印刷厂家-湖南省日大彩色印务有限公司 青州搬家公司电话_青州搬家公司哪家好「鸿喜」青州搬家 |