Methodのinvokeメソッド実装のJavaリフレクション
フレームワークでは、オブジェクトのターゲットメソッドを実行するために、method.invoke()がよく使われます。以前、リフレクションを使ってコードを書いていたときは、必ず最初にMethodを取得し、対応するClassインスタンスオブジェクトを渡してメソッドを実行していました。しかし、少し前にinvokeメソッドについて調べたところ、実はinvokeメソッドにはポリモーフィックな機能が含まれていることがわかり、今まで考えもしなかった問題が出てきました。では、Method.invoke()メソッドの実行処理はどのように実装されているのでしょうか?また、そのポリモーフィズムはどのように実装されているのでしょうか?
今回は、javaのソースコード実装とJVMからinvokeメソッドの実装を深堀りしていきます。
まず、invokeメソッドのポリモーフィック性を実証するコードを示す。
public class MethodInvoke { public static void main(String[] args) throws Exception { { メソッド animalMethod = Animal.class.getDeclaredMethod("print"); Method catMethod = Cat.class.getDeclaredMethod("print"); Animal animal = new Animal(); Cat cat = new Cat(); animalMethod.invoke(cat)を実行します。 animalMethod.invoke(animal)を実行します。 catMethod.invoke(cat)を実行します。 catMethod.invoke(animal)。 } } クラス アニマル { public void print() { System.out.println("Animal.print()") とします。 } class Cat extends Animal { オーバーライド public void print() { System.out.println("Cat.print()")); } }このコードでは、Catクラスは親クラスであるAnimalクラスのprint()メソッドをオーバーライドし、リフレクションによってprint()のMethodオブジェクトを個別に取得しています。最後に、CatとAnimalのインスタンスオブジェクトを使って、それぞれprint()メソッドを実行します。animalMethod.invoke(animal) と catMethod.invoke(cat) では、サンプルオブジェクトの実型とメソッドの宣言クラスが同じなので、期待通りの結果が出力されます。animalMethod.invoke(cat) では、AnimalのサブクラスであるCatが使用されているので、期待通りの結果が出力されます。animalMethod.invoke(cat) では、Cat は Animal のサブクラスなので、polymorphic 属性に従って、サブクラスは親クラスのメソッドを呼び出し、メソッドの実行はサブクラスの実装に動的にリンクされます。catMethod.invoke(animal) では、渡されたパラメータ型 Animal は親クラスですが、サブクラス Cat のメソッドを呼び出すことを期待しているので、今回は例外がスローされます。このコードでは、結果を次のように出力しています。
Cat.print() Animal.print() Cat.print() Exception in thread "main" java.lang.IllegalArgumentException: object is not an instance of declaring class at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) Method.invoke(Unknown Source) at java.lang.reflect. at com.wy.invoke.MethodInvoke.main(MethodInvoke.java:17)
次に、invoke()メソッドの実装を見てみましょう。
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<? > caller = Reflection.getCallerClass(1); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }
invoke()メソッドには、アクセス制御のチェックと、メソッド実行を実装するためのMethodAccessor.invoke()の呼び出しという、2つの主要な部分があります。
まず、アクセス制御のチェック部分のロジックを見てみましょう。一見すると、ここのロジックは何のためにあるのか混乱してしまいがちです。平たく言えば、メソッドの呼び出し元がその修飾子(public/protected/private/package)でメソッドにアクセスできるかどうかを判断するためのものです。これはjavaの基本的な内容ですが、コードで書き出すと、なかなか一度には思いつきません。アクセス制御のチェックは、3つのステップに分かれています。
- オーバーライドをチェックし、オーバーライドが真であればチェックをスキップし、そうでなければ続行します。
- メソッドの修飾子が public であるかどうかを簡単にチェックします。
- 呼び出し元がその (protected/private/package) 修飾子によって、あるいはメソッドの宣言クラス (たとえばサブクラスが親の protected メソッドにアクセスできる) と呼び出し元の関係によってメソッドにアクセスする許可を持っているかどうかを調べる詳細なチェックです。
override属性は、Methodの親クラスAccessibleObjectで宣言された変数で、アクセス許可チェックをスキップするかどうかをプログラムで制御できるようにするものである。また、Override属性の初期値は、Methodのインスタンス・オブジェクトでfalseに設定される。
public void setAccessible(boolean flag) throws SecurityException { SecurityManager sm = System.getSecurityManager(); if (sm ! = null) sm.checkPermission(ACCESS_PERMISSION); setAccessible0(this, flag); } private static void setAccessible0(AccessibleObject obj, boolean flag) throws SecurityException { if (obj instanceof Constructor && flag == true) { Constructor<? > c = (Constructor<? >)obj; if (c.getDeclaringClass() == Class.class) { throw new SecurityException("Can not make a java.lang.Class" + " constructor accessible"); } } obj.override = flag; }
余談ですが、FieldもAccessibleObjectを継承しており、Fieldのオーバーライドもfalseに初期化されているので、変数のモディファイアによって異なる値が初期化されることはないです。しかし、Field.set(Object obj, Object value)を呼ぶと、Fieldがprivateで変更された場合、アクセス権がないため例外が発生しますので、setAccessible(true)を呼ぶ必要があります。ここでは、変数がpublicなので、overrideはtrueに初期化されていることが非常にわかりやすい。
invoke()メソッドでアクセス制御のチェックを行った後、MethodAccessor.invoke()で呼び出されるのは、このメソッドです。もう一度コードを見てみると
MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args);
ここでのロジックは単純で、まず変数 methodAccessor を ma に代入し、直接参照できるローカル変数をメソッドスタックに保持し、もし methodAccessor が存在しなければ acquireMethodAccessor() メソッドを呼び出して作成する、というものです。
private volatile MethodAccessor methodAccessor; private Method root; private MethodAccessor 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; } else { // Otherwise fabricate one and propagate it up to the root tmp = reflectionFactory.newMethodAccessor(this); newMethodAccessor(this); setMethodAccessor(tmp); } return tmp; } void setMethodAccessor(MethodAccessor accessor) { methodAccessor = accessor; // Propagate up if (root ! = null) { root.setMethodAccessor(accessor); } } Method copy() { Method res = new Method(clazz, name, parameterTypes, returnType, exceptionTypes, modifiers, slot, signature, annotations, parameterAnnotations, annotationDefault); res.root = this; res.methodAccessor = methodAccessor; return res; }
acquireMethodAccessor()、setMethodAccessor()、copy()メソッドを組み合わせると、Methodインスタンスオブジェクトがルート参照を保持していることが分かります。メソッドをコピーするためにMethod.copy()が呼び出されると、ルートはコピーされるオブジェクトを指します。そして、Methodが複数回コピーされると、setMethodAccessor()メソッドを一回呼び出すと、ルート参照が指すMethodのmethodAccessor変数に同じ値が代入されます。例えば D -> C -> B -> A, X -> Y は X = Y.copy() を意味し、C オブジェクトが setMethodAccessor() を呼び出すと、B と A の両方で methodAccessor の割り当てが伝播し、Dの methodAccessor はまだ null のままです。
Methodのルート参照を維持する意図は、常に同じメソッドに1つのmethodAccessorインスタンスしか持たせないことですが、上記の方法はまだ、同じメソッドが1つのmethodAccessorインスタンスしか持たないことを保証するものではありません。例えば、copy()を使ってABCDの関係を維持する場合、ABCD: D -> C -> B -> A の関係を維持するために、B が setMethodAccessor() を呼び出すと、B と A の両方が methodAccessor を割り当て、C と D はまだ null methodAccessor を持つことになります。DがacquireMethodAccessor()を呼び出すと、DはCのmethodAccessorであるルートを取得し、それがnullであることがわかり、新しいものを作成する。このように、同じメソッド内に2つのmethodAccessorインスタンスオブジェクトが存在することになります。
Class.getMethod(), Class.getDeclaredMethod(), Class.getDeclaredMethod(String name, Class<? >... parameterTypes) メソッドでは、最終的に copy() メソッドを呼び出してメソッドの使用を確保するようにします。 より極端な例+偶然の例では、これはクラスの肥大化の問題を引き起こす可能性があります。これは、次に説明するMethodAccessorを実装するためのメカニズムです。
先のコードでは、ReflectionファクトリーであるReflectionFactoryのnewMethodAccessor(Method)メソッドを用いてMethodAccessorを作成しました。
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; } }
checkInitted()メソッドが、設定項目から設定を読み込んで、noInflation, inflationThresholdの値を設定することを確認するところ。
private static void checkInitted() { if (initiated) return; AccessController.doPrivileged( new PrivilegedAction<Void>() { public Void 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; } }); }
これは、起動パラメータ -Dsun.reflect.noInflation=false -Dsun.reflect.inflationThreshold=15 で設定することができます。
文字通りの意味と以下のコードを組み合わせると、2つの設定 sun.reflect.noInflation はクラスのインフレを直ちに行うかどうかを制御し、 sun.reflect.inflationThreshold はクラスのインフレの閾値を設定します。
一つは、sun.reflect.noInflation 設定項目が true の場合、ReflectionFactory は MethodAccessor バイトコード生成クラス MethodAccessorGenerator を使用して直接プロキシを作成します MethodAccessor の別の実装方法として DelegatingMethodAccessorImpl 代表クラスを作成して invoke() メソッドの実行に関する詳細は NativeMethodAccessorImpl に任せ、 NativeMethodAccessorImpl が最終的にネイティブメソッドを呼んで invoke() 作業が完結するようにする方法があります。以下は、NativeMethodAccessorImplによるinvoke()メソッドの実装です。
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); } private static native Object invoke0(Method m, Object obj, Object[] args);
numInvocations の数が設定項目 sun.reflect.inflationThreshold というクラスのインフレ閾値より大きい場合、MethodAccessorGenerator を使ってプロキシクラスオブジェクトが生成され、委譲先のクラス DelegatingMethodAccessorImpl の親が、この生成したプロキシオブジェクトに設定されていることがわかります。ちょっと回りくどいので、図解で流れを説明します。
全体として、invoke()を呼び出す場合、デフォルトの設定通り、まずMethodがDelegatingMethodAccessorImplオブジェクトを生成して、委譲先のNativeMethodAccessorImplオブジェクトを設定し、method.invoke()がDelegatingMethodAccessorImpl.invoke()に変換されて、その実装に委ねられることになります。NativeMethodAccessorImp.invoke()が一定回数以上(デフォルト15回)呼ばれると、委譲された側は再びプロキシクラスに変換されて実装されます。
同じメソッドのメソッドオブジェクトの異なるコピーが複数存在する極端なケースで前述したように、MethodAccessorオブジェクトが複数存在する場合があります。そうすると、複数回呼び出されたときに、必然的に機能が重複した2つのプロキシクラスが生成されることになります。もちろん、一般的にはプロキシクラスが2つ生成されても大きな影響はない。
プロキシクラスの具体的なバイトコード実装がより複雑な場合は、以下のようなクラスを生成するのが一般的な考え方です。
public class GeneratedMethodAccessor1 extends MethodAccessorImpl { public GeneratedMethodAccessor1 () { super(); } public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { if (! (obj instanceof Cat)) { throw new ClassCastException(); } if (args ! = null && args.length ! = 0) { throw new IllegalArgumentException(); } try { Cat cat = (Cat) obj; cat.print(); return null; } catch (Throwable e) { throw new InvocationTargetException(e, "invoke error"); } } }
ここまでのところ、プロキシの GeneratedMethodAccessor1 クラスを除き、メソッドの実行はポリモーフィックであり、NativeMethodAccessorImp の invoke() の実装は jdk で行われています。次に、NativeMethodAccessorImp のネイティブメソッド invoke0() に移動します。
まず、NativeAccessors.cを探します。Java_sun_reflect_NativeMethodAccessorImpl _invoke0()メソッドがあり、JNIの関数名定義のルールに従い、 "package_class_name_method_name" これが探しているネイティブメソッド実装エントリになります。
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_で始まるメソッドコールJVM_InvokeMethod()は、jvm.cppファイルで定義されており、ヘッダーファイルjvm.hで見ることができますので、ご存じでない方はご覧になってみてください。慣れないうちは、ヘッダーファイルjvm.hで確認できます。そのままトレースすると、jvm.cppはspotsrcshareのjvmprimsというフォルダーにあります。
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
ここで oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL) is execution of the method, found in the \hotspotsrcsharevmruntime path reflection.cpp file.
oop 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_Klass(mirror)); Method* 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); } oop Reflection::invoke(instanceKlassHandle klass, methodHandle reflected_method, Handle receiver, bool override, objArrayHandle ptypes, BasicType rtype, objArrayHandle args, bool is_method_invoke, TRAPS) { ResourceMark rm(THREAD); methodHandle method; // actual method to invoke KlassHandle target_klass; // target klass, receiver's klass for non-static // Ensure klass is initialized klass->initialize(CHECK_NULL); bool is_static = reflected_method->is_static(); if (is_static) { // ignore receiver argument method = reflected_method; target_klass = klass; } else { // check for null receiver if (receiver.is_null()) { THROW_0(vmSymbols::java_lang_NullPointerException()); } // Check class of receiver against class declaring method if (!receiver->is_a(klass())) { THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "object is not an instance of declaring class"); } // target klass is receiver's klass target_klass = KlassHandle(THREAD, receiver->klass()); // no need to resolve if method is private or <init> if (reflected_method->is_private() || reflected_method->name() == vmSymbols::object_initializer_name()) { method = reflected_method; } else { // resolve based on the receiver if (reflected_method->method_holder()->is_interface()) { // resolve interface call if (ReflectionWrapResolutionErrors) { // new default: 6531596 // Match resolution errors with those thrown due to reflection inlining // Linktime resolution & IllegalAccessCheck already done by Class.getMethod() method = resolve_interface_call(klass, reflected_method, target_klass, receiver, THREAD); if (HAS_PENDING_EXCEPTION) { // Method resolution threw an exception; wrap it in an InvocationTargetException oop resolution_exception = PENDING_EXCEPTION; CLEAR_PENDING_EXCEPTION; JavaCallArguments args(Handle(THREAD, resolution_exception)); THROW_ARG_0(vmSymbols::java_lang_reflect_InvocationTargetException(), vmSymbols::throwable_void_signature(), &args); } } else { method = resolve_i } else { ResourceMark rm(THREAD); THROW_MSG_0(vmSymbols::java_lang_AbstractMethodError(), Method::name_and_sig_as_C_string(target_klass(), method->name(), method->signature())); } } } } } } // I believe this is a ShouldNotGetHere case which requires // an internal vtable bug. If you ever get this please let Karen know. if (method.is_null()) { ResourceMark rm(THREAD); THROW_MSG_0(vmSymbols::java_lang_NoSuchMethodError(), Method::name_and_sig_as_C_string(klass(), reflected_method->name(), reflected_method->signature())); } // In the JDK 1.4 reflection implementation, the security check is // done at the Java level if (! (JDK_Version::is_gte_jdk14x_version() && UseNewReflection)) { // Access checking (unless overridden by Method) if (!override) { if (! (klass->is_public() && reflected_method->is_public())) { if (! bool access = Reflection::reflect_check_access(klass(), reflected_method->access_flags(), target_klass(), is_method_invoke, CHECK_NULL ); if (!access) { return NULL; // exception } } } } // ! (Universe::is_gte_jdk14x_version() && UseNewReflection) assert(ptypes->is_objArray(), "just checking"); int args_len = args.is_null() ? 0 : args->length(); // Check number of arguments if (ptypes->length() ! = args_len) { THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "wrong number of arguments"); } // Create object to contain parameters for the JavaCall JavaCallArguments java_args(method->size_of_parameters()); if (!is_static) { java_args.push_oop(receiver); } for (int i = 0; i < args_len; i++) { oop type_mirror = ptypes-> obj_at(i); oop arg = args-> obj_at(i); if (java_lang_Class::is_primitive(type_mirror)) { jvalue value; BasicType ptype = basic_type_mirror_to_basic_type(type_mirror, CHECK_NULL); BasicType atype = unbox_for_primitive(arg, &value, CHECK_NULL); if (ptype ! = atype) { widen(&value, atype, ptype, CHECK_NULL); } switch (ptype) { case T_BOOLEAN: java_args.push_int(value.z); break; case T_CHAR: java_args.push_int(value.c); break; case T_BYTE: java_args.push_int(value.b); break; case T_SHORT: java_args.push_int(value.s); break; case T_INT: java_args.push_int(value.i); break; case T_LONG: java_args.push_long(value.j); break; case T_FLOAT: java_args.push_float(value.f); break; case T_DOUBLE: java_args.push_double(value.d); break; default: THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "argument type mismatch"); } } else { if (arg ! = NULL) { Klass* k = java_lang_Class::as_Klass(type_mirror); if (!arg->is_a(k)) { THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "argument type mismatch"); } } Handle arg_handle(THREAD, arg); // Create handle for argument java_args.push_oop(arg_handle); // Push handle } } assert(java_args.size_of_parameters() =
Reflection::invoke_method() は Reflection::invoke() を呼び出し、さらに Reflection::invoke() メソッドの中で、Reflection によって呼び出されたメソッドがインターフェースメソッドの場合、 Reflection::resolve_interface_call( ) に依存し、そのメソッドの動的リンク処理を完結させていますが、ここではその具体的実装は示しません。
method = resolve_interface_call(klass, reflected_method, target_klass, receiver, CHECK_(NULL)); methodHandle Reflection::resolve_interface_call(instanceKlassHandle klass, methodHandle method, KlassHandle recv_klass, Handle receiver, TRAPS) { assert(!method.is_null() , "method should not be null"); CallInfo info; Symbol* signature = method->signature(); Symbol* name = method->name(); LinkResolver::resolve_interface_call(info, receiver, recv_klass, klass, name, signature, KlassHandle(), false, true, CHECK_(methodHandle())); return info.selected_method(); }
// if the method can be overridden, we resolve using the vtable index. assert(!reflected_method->has_itable_index(), ""); int index = reflected_method->vtable_index(); method = reflected_method; if (index ! = Method::nonvirtual_vtable_index) { // target_klass might be an arrayKlassOop but all vtables start at // the same place. // The cast is to avoid virtual call and assertion. InstanceKlass* inst = (InstanceKlass*)target_klass(); method = methodHandle(THREAD, inst->method_at_vtable(index)); }
Reflection によって呼び出されたメソッドが Animal.print() のようにオーバーライドできるものである場合、Reflection::invoke() は最終的に、仮想メソッドテーブル vtable を照会して最終メソッドを決定することになります。
// if the method can be overridden, we resolve using the vtable index. assert(!reflected_method->has_itable_index(), ""); int index = reflected_method->vtable_index(); method = reflected_method; if (index ! = Method::nonvirtual_vtable_index) { // target_klass might be an arrayKlassOop but all vtables start at // the same place. // The cast is to avoid virtual call and assertion. InstanceKlass* inst = (InstanceKlass*)target_klass(); method = methodHandle(THREAD, inst->method_at_vtable(index)); }
概要
1. method.invoke() メソッドはポリモーフィック機能をサポートし、そのネイティブ実装はメソッドが実際に実行される前に動的結合またはダミーメソッドテーブルによって実行されます。
2. フレームワークでメソッド呼び出しを実行するために method.invoke() を使用する場合、最初にメソッドオブジェクトを取得するときに一度 setAccessable(true) を呼び出すと、後で invoke() を呼び出すたびにメソッド修飾子の決定が省け、パフォーマンスがわずかに向上します。Fieldもビジネスで許されるのであれば、同じようなことができます。
3. デリゲートパターンは、ソリューションの複数の実装を切り替える自由度に対応し、プロキシパターンは、渡されたオブジェクトに基づく機能のみを実装することができます。
参考記事
関連
-
Java のエラーです。未解決のコンパイル問題 解決方法
-
プロジェクトの依存関係を解決できなかった 解決
-
javaコンパイル時のエラー:不正な文字 '\ufeff' に対する解決策です。
-
spring aop アドバイスからの Null 戻り値が、サマリーのプリミティブ戻り値と一致しない。
-
Eclipse起動エラー:javaは起動したが、終了コード=1を返した(ネット上の様々な落とし穴)
-
HttpClientがGZIP形式でない場合の対処法
-
Java-Myeclipse エラー解決 構文エラー、TryStatem を完了するために "Finally" を挿入する。
-
Java面接のポイント3--例外処理(Exception Handling)
-
Android TextViewの行間解析
-
アイデア2021.2が起動しないことを一度記録した
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
この行に複数のマーカーがある - HttpServletResponseが型エラーに解決できない
-
Javaクラスローダーにソースコードから潜り込む
-
スキャナは、タイプに解決することはできません最もルーキー初心者の質問
-
プロローグでのコンテンツは禁止されています
-
VMの初期化中にエラーが発生しました java/lang/NoClassDefFoundError: java/lang/Object
-
スレッド "main" での例外 java.lang.ArrayIndexOutOfBoundsException: 1
-
[オリジナル】java学習ノート【II】よくあるエラー クラスパス上のクラスファイルが見つからない、またはアクセスできない場合
-
無効なカラム名、エラーは完全に解決
-
Tomcat 8は、「少なくとも1つのJARがTLDをスキャンされたが、TLDが含まれていない」問題を解決します。
-
javaException: 比較メソッドが一般契約に違反しています!