Salut,
Dans le cadre d'un projet, j'ai plusieurs contextes liés à des versions différentes d'un serveur. J'ai une version de bibliothèque de connexion différente par version de serveur (Il s'agit d'un JAR propriétaire + des JARs Apache (commons, httpclient...). Afin de permettre à plusieurs contextes de cohabiter dans une même application, j'ai décidé de charger dynamiquement les bibliothèques de connexion et de tout invoquer par réflexion, par l'intermédiaire d'une API de proxies fondée sur java.lang.invoke (première fois que je pratique) et JCL (et son JarClassLoader).
Cela fonctionne toujours la première fois (je fais un petit programme de test, l'API n'étant que partiellement implémentée, juste pour valider que ça fonctionne). Mais j'obtiens une LinkageError ensuite, lors d'un autre test (même code), sur la création d'un handler(MethodHandle) sur une méthode : le plus souvent la seconde fois, mais j'ai eu un cas de tests où ça plantait à partir de la troisième fois (Je décris plus loin les 2 cas de tests). L'erreur de Linkage survient lors du lookup d'une méthode par MethodHandles.publicLookUp() : je ne sais pas si le problème vient de ma mauvaise utilisation de l'API java.lang.invoke ou est lié à ma mauvaise utilisation du JarClassLoader JCL, ou à l'environnement ou l'implémentation de la bibliothèque "dynamique", ou autre.
Voici la stacktrace :
Tout d'abord, j'ai un peu du mal à comprendre pourquoi le bootloader est cité dans l'histoire, et surtout comment pourrait-il avoir chargé une version de l'interface xxx/interfaces/IServer sachant qu'elle n'est pas dans le (son) classpath. A priori, de ce que je comprends, la classe java.lang.Object citée est celle de base pour le lookup de méthode : en quoi le classloader de cette classe doit-il intervenir dans la résolution des types de la méthode recherchée.Caused by: java.lang.LinkageError: loader constraint violation: when resolving method "xxx.impl.common.ServerFactory.getRemoteServer(Ljava/lang/String;)Lxxx/interfaces/IServer;" the class loader (instance of <bootloader>) of the current class, java/lang/Object, and the class loader (instance of xxx/engine/db/internal/RemoteClientJarClassLoader) for the method's defining class, xxx/impl/common/ServerFactory, have different Class objects for the type xxx/interfaces/IServer used in the signature
at java.lang.invoke.MethodHandleNatives.resolve(Native Method)
at java.lang.invoke.MemberName$Factory.resolve(MemberName.java:962)
at java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:987)
... 56 more
D'après les traces, les classes citées dans la stacktrace sont au moins bien chargées par le JarClassLoader JCL. Je comprends bien le problème d'incompatibilité des classes, mais en l'occurance, je ne vois pas pourquoi je devrais être compatible avec des classes d'un autre contexte qui n'est absolument pas censé manipuler les classes que je manipule. Est-ce que mon problème serait lié à l'environnement OSGi dans lequel mon application fonctionne (Eclipse Platform en l’occurrence) ? A noter, que j'ai déjà un autre composant qui fonctionne très bien, et autant de fois que nécessaire, même en cas d'exception, mais qui est beaucoup plus simple : un seul JAR, aucune des méthodes invoquées n'a de type de ce JAR en tant qu'argument ou type de retour, et je n'utilisais pas l'API java.lang.invoke, mais java.lang.reflect.
En tout cas, si quelqu'un voit où j'ai fait ce qu'il ne fallait pas, ou pas fait ce qu'il fallait, merci d'avance de m'en faire part.
Quelques précisions supplémentaires :
Chaque proxy est instancié par une fabrique utilisant le JarClassLoader en passant l'instance de classe dynamique, elle-même instanciée par réflexion. Pour me faciliter l'écriture des proxies, je peux passer, lors du lookup (de méthodes ou de constructeurs), la classe de proxy (ou l'instance selon la méthode), celle-ci étant adaptée au besoin (adaptClass() dans le code ci-après) en classe dynamique via une constante de la classe de proxy (voir pour exemple PROXIED_CLASS ci-après).
L'erreur relative à la stacktrace ci-dessus correspond à l'exécution du constructeur ci-après :
RemoteClienJarClassLoader est une extension de JarClassLoader (JCL). L'exception intervient lors de l'appel de la seconde ligne du constructeur ci-dessus. Les interfaces de mon API de proxy ont le même nom simple que celle du JAR dynamique, mais pas le même package.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 public class ServerFactoryImpl extends AbstractRemoteLibraryClassProxy implements IServerFactory { public static final String PROXIED_CLASS = "xxx.impl.common.ServerFactory"; //$NON-NLS-1$ private static final String GET_REMOTE_SERVER = "getRemoteServer"; //$NON-NLS-1$ private final MethodHandle getRemoteServerMethod; public ServerFactoryImpl(RemoteClientJarClassLoader classLoader, Object instance) throws CoreException { super(classLoader, instance); getRemoteServerMethod = classLoader.getMethod(instance, GET_REMOTE_SERVER, ServerImpl.class, String.class); } public IServer getServer(String url) throws CoreException { try { Object remoteServer = getRemoteServerMethod.invoke(url); return new ServerImpl(classLoader, remoteServer); } catch(CoreException e) { throw e; } catch (Throwable e) { throw new CoreException(XXXActivator.createErrorStatus("Cannot get server", e)); //$NON-NLS-1$ } }
La méthode getMethod() :
Voici les 2 cas de tests :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 public MethodHandle getMethod(Object instance, String methodName, Class<?> returnType, Class<?>...types) throws CoreException { try { return PUBLIC_LOOKUP.bind(instance, methodName, MethodType.methodType( adaptClass(returnType), Arrays.stream(types).map(t-> uncheckedAdaptClass(t)).toArray(n-> new Class[n]))); } catch ( RaiseException e ) { throw e.raise(t-> new CoreException(XXXActivator.createErrorStatus("Cannot get method " + methodName + " in class " + instance.getClass(),t)), //$NON-NLS-1$ //$NON-NLS-2$ CoreException.class); } catch (NoSuchMethodException | IllegalAccessException e ) { throw new CoreException(XXXActivator.createErrorStatus("Cannot get method " + methodName + " in class " + instance.getClass(), e)); //$NON-NLS-1$ //$NON-NLS-2$ } }
- Celui qui fonctionne la première fois et plante dès la seconde
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 try (DAOHandler dao = xxxProject.getDAOHandler()) { // DAOHandler encapsule le JarClassLoader, ainsi qu'une instance de proxy (ServerFactory=>ServerFactory) System.out.println(dao); IServer server = dao.getServer(); // invoque indirectement ServerFactory pour obtenir un proxy IServer=>IServer (en fait l'invocation est faite dans le constructeur et l'instance est stockée, getServer() se comportant que comme un getter). System.err.println(server); ISession session = dao.getServer().getSession("admin","admin"); // invoque directement IServer pour obtenir un proxy ISession=>ISession System.out.println(session); } catch (CoreException e) { e.printStackTrace(); } catch (IOException e1) { e1.printStackTrace(); }- Celui qui fonctionne la première fois, la seconde et plante dès la troisième
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 try (DAOHandler dao = xxxProject.getDAOHandler()) { // DAOHandler encapsule le JarClassLoader, ainsi qu'une instance de proxy (ServerFactory=>ServerFactory) System.out.println(dao); IServer server = dao.getServer(); // invoque indirectement ServerFactory pour obtenir un proxy IServer=>IServer System.err.println(server); } catch (CoreException e) { e.printStackTrace(); } catch (IOException e1) { e1.printStackTrace(); }
On voit que les 2 tests invoquent la méthode (getServer()) sur laquelle le lookup plante.
S'il y a besoin d'autres précisions, extraits de code... n'hésitez pas.
Partager