Créer un système de plugin

En java :p

a marqué ce sujet comme résolu.

Bonjour,

Je viens solliciter votre aide, car j'aimerais pouvoir créer un système de plugin. Mais n'ayant jamais fais ça, je suis un peu perdu. Et les tutoriels à ce niveau sur internet, et bien il n'y en a pas beaucoup.

Quand j'étais plus petit, je m'amusait à créer des plugins pour des serveurs Minecraft, j'aimerais pouvoir faire un système relativement similaire.

Comment dois-je m'y prendre ?

En vous remerciant, WinXaito.

C'est assez vague (et vaste) comme question.

Une façon de faire que je connais plutôt bien c'est d'écrire une API, de ton côté, donc un ensemble de classes et de méthodes qui font des trucs. Ca c'est vraiment la base, écrire l'API qu'un développeur pourrait manipuler pour faire ce que tu veux qu'il fasse dans ton plugin (déplacer un fichier, envoyer un email, et que sais-je encore). Je vais écrire les exemples en Groovy parce que c'est un langage que je connais bien donc je suis plus à même de t'expliquer le cheminement.

1ère étape :

1
2
3
4
5
import com.winxaito.starwarsapi.Base
import com.winxaito.starwarsapi.BaseRegistry

def bases = [new Base('Tatooine', 'Luke', 57), new Base('Naboo', 'Padme', 107)] // ton API lui fournit de quoi créer des bases
BaseRegistry.registerMyBases(bases) // là tu vas faire un appel à ta BDD ou autre
1
groovy SonScript.groovy -cp starwarsapi.jar #un peu pénible de se trimballer ton jar...

Déjà t'as une belle première étape, un développeur peut écrire un plugin "en code", il écrit un bout de code qui se sert ton API, te l'envoie, tu le fais tourner chez toi, et roulez. Mais c'est pas bien pratique, parce qu'il faut qu'il embarque ton API pour compiler son code par exemple, donc ton jar, ta gem, etc.

Donc le mieux c'est que tu lui prépares un environnement chez toi dans lequel il va faire tourner ses scripts. On appelle ça un shell parfois, et certains langages te permettent directement de "lire et interpréter" des chaînes de caractères comme si c'était du code (Groovy Shell, Javascript eval, …). Et généralement, en préparant l'environnement, bah tu lui mâches le travail.. Là c'est déjà sympa, le mec écrit :

1
2
3
4
5
// Youpi tu lui as mis les imports dans ton shell histoire qu'il se les trimballe pas

bases << new Base('Tatooine', 'Luke', 57) // la liste de bases existe déjà, c'est toi qui la lui donnes
bases << new Base('Naboo', 'Padme', 107)
// pas besoin d'appeler d'autre méthode, comme tu lui as filé la liste, tu es capable de l'instrumenter, et donc de savoir que quand elle a changé de taille en exécutant son script c'est qu'il faut la sauvegarder en BDD

Ensuite il existe des mécaniques plus avancées, parmi celles que je connais y'a le système de sandbox. En gros tu dis "ton plugin c'est un script, et quand tu fais ton script, chez moi je vais le faire tourner dans un environnement cloisonné, restreint, pour que tu bricoles pas avec mon système par exemple". La plupart des langages qui ont été pensés pour écrire des plugins ou des extensions fournissent généralement ce genre de fonctionnalités. Groovy, Javascript pour ceux que je connais mais ça m'étonnerait pas que l'interpréteur Ruby permette de le faire par exemple. Histoire que le mec écrive pas

1
2
3
4
5
6
bases << new Base('Tatooine', 'Luke', 57)
while(true) {
  Runtime.getRuntime().exec(["javaw", "-cp", System.getProperty 'java.class.path', "ForkBomb"])
} // lolilol, ça va bien ton serveur ? :D
// ou bien marrant aussi : 
System.exit() // prout :P

Donc à cette étape t'es "production-ready" déjà, n'importe qui peut t'écrire un plugin et le faire fonctionner avec (au moins un peu de) sécurité, chez toi, pépère. Tu peux même t'amuser a brancher ça sur un textarea ou autre histoire qu'on écrive des plugins directement sur ton serveur.

Enfin, tu vas peut-être vouloir que les gens qui ne sont pas nécessairement développeurs écrivent des plugins, dans ce cas, il faut leur fournir une espèce de surcouche à ton API, une "façon d'écrire" différente du code pur et dur et des appels de méthodes. En règle générale on appelle cela une DSL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bases {
  Tatooine {
    gouverneur: 'Luke'
    resources: 57
  }
  Naboo {
    gouverneur: 'Padme'
    resources: 107
  }
}

Au final, ce code branché sur ton API à travers une DSL fait exactement la même chose que les autres. Simplement pour un mec qui ne sait pas programmer, ça pourra lui parler un peu plus.

Si tu connais Gradle et que tu as déjà écrit un fichier build.gradle pour Android par exemple, tu as utilisé sans le savoir une DSL écrite par les gens de chez Android.

EDIT : En me relisant, j'ai pas fait exprès mais j'ai pris un exemple de client / serveur pour lequel les plugins s'exécutent sur le serveur, c'est déjà un peu différent du scripting de jeu. Donc tu vois, même en essayant de rester générique, je suis déjà rentré dans un cas assez particulier. Faut vraiment essayer de nous dire dans quel contexte tu veux pouvoir permettre de créer des plugins, ce dont tu disposes aujourd'hui, comment fonctionne ton logiciel, etc. Là on va commencer à pouvoir mieux t'aiguiller.

+0 -0

Je parle ici d'une API programmatique hein, pas d'une API REST, va pas confondre les deux et partir sur une fausse piste.

Des APIs tu en utilises tous les jours quand tu codes en Java. Notamment L'API standard, par exemple java.util qui comprend tout plein d'interfaces intéressantes.

Ici, tu vas fournir un ensemble d'interfaces et de méthodes à tes clients pour qu'ils puissent manipuler ton programme. Je ne sais pas trop si c'est clair, mais y'a vraiment pas beaucoup de recherches à faire, expose juste des interfaces publiques, cache leurs implémentations (vivement Java 9), package ça dans un jar et tu auras déjà ta première étape.

+1 -0

Merci je vois.

Par contre je ne comprend pas, pour tester j'ai créer deux nouveau projet, contenant respectivement ceci:

PluginManager

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.winxaito.main;

public class MainApp{
    public static void main(String[] args) {
        System.out.println("Salut");
    }


    public void justAFunction(){
        System.out.println("Je suis juste une fonction appelé par un plugin :p");
    }
}

Plugin

1
2
3
4
5
6
7
import com.winxaito.main.MainApp;

public class Mian{
    MainApp main = new MainApp();

    main. //Ici problème, je n'ai accès a aucune fonction.
}

J'ai donc ajouté en tant que librairie PluginManager dans Plugin, j'ai bien la possibilité d'importer MainApp, mais je n'ai accès à aucune fonction (Ni main(), ni justAFunction())

Est-ce que je m'y prend mal ?

En fait, c'est un problème Java. Ce comportement est normal pour 2 raisons :

  • primo, les méthodes de MainApp ne renvoient que void dans l'exemple que tu donnes. On ne peut pas affecter void à une variable.
  • secondo, on ne peux pas à ma connaissance, appeler une méthode qui renvoie void directement dans une classe. On est obligé de le faire dans un méthode.

L'exemple suivant illustre cela :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.winxaito.main;

public class MainApp{
    public static void main(String[] args) {
        System.out.println("Salut");
    }

    public void justAFunction(){
        System.out.println("Je suis juste une fonction appelée par un plugin :p");
    }

    public int giveMe5(){ return 5; }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import com.winxaito.main.MainApp;

public class Mian{
    MainApp main = new MainApp();

    int five = main.giveMe5(); // Revoie autre chose que void -> OK

    public void test(){
        main.justAFunction(); // Renvoie void -> on est dans un fonction -> OK
    }
}

EDIT : typo

+1 -0

Ah oui bien sûr, dans la précipitation j'ai oublier d'ajouter une méthode. Merci beaucoup, je testerai ça se soir !

Sinon encore une petite question, maintenant que j'ai réussi a réaliser mon "api" et mon plugin. Il va falloir que je le charge dans mon programme, donc il me semble qu'il faut partir sur des "ClassLoader" ou quelque chose comme ça. Est-ce correct?

Salut,

En fait, le principe de base peut être assez simple, on décrit les API sous forme d'interface, et après on charge des instances concrètes en utilisant la réflexion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public interface Plugin {
public int compute (int a, int b);
}

public class Plugin1 implements Plugin {
public int compute (int a, int b) { return a+b; }
}

public class Plugin2 implements Plugin {
public int compute (int a, int b) { return a*b; }
}


public class Application {
public static void main (String[] args) {
Plugin p = (Plugin) (Class.forName(args[0]) .newInstance());
int result = p.compute(7, 12);
System.out.println(result);
}}
1
2
3
4
> java Application Plugin1
19
> java Application Plugin2
84

En réalité on chargera les classes de plugin d'une façon un peu plus sophistiquée, en utilisant un ClassLoader personnalisé. Mais le concept de base est là.

+1 -0

J'ai testé un petit truc, si quelqu'un a un petit moment pour me dire si c'est correct ?

Le plugin Manager: https://github.com/WinXaito/zest-writer/blob/plugins/src/main/java/com/zestedesavoir/zestwriter/plugins/PluginsManager.java
Le plugin Loader: https://github.com/WinXaito/zest-writer/blob/plugins/src/main/java/com/zestedesavoir/zestwriter/plugins/PluginsLoader.java

(Bon la j'ai encore quelques soucis, d'où tous les "System.out.println()")

Il manque l'instaciation de la classe dans ton exemple si j'ai pas lu trop vite.

En effet. C'est corrigé.

C'était évident, mais j'ai volontairement éludé la gestion des exceptions dans ce code pour le rendre plus simple. Class.forName, Class.newInstance et le cast peuvent lancer 3 ou 4 types d'exceptions différentes.

+0 -0

Une petite question, maintenant que j'ai pu créer mon Loader, j'aimerais savoir quelle est la meilleur solution pour par exemple pouvoir géré les événements de l'application (Redimensionnement de la fenêtre, etc.)

Je ne sais pas si je peux passer par des interfaces ? Mais je ne sais pas trop comment faire pour pouvoir exécuter les événements dans le plugin (Et le but est pas de rendre la définition des événements dans le plugin obligatoire).

En vous remerciant.

Je ne sais pas si je peux passer par des interfaces ? Mais je ne sais pas trop comment faire pour pouvoir exécuter les événements dans le plugin (Et le but est pas de rendre la définition des événements dans le plugin obligatoire).

Avec Java 8, tu peux définir des implémentations par défaut de méthodes dans les interfaces. Dans les faits ça rend effectivement la réécriture facultative pour l'implémenteur.

Sinon, si tu est coincé sur Java 6 ou 7, rien ne t'interdit d'utiliser des classes abstraites (des adapteurs), voire même éventuellement des classes concrètes, à la place des interfaces. Dans le dernier cas ça rend l'architecture moins claire et ça peut introduire des failles donc ce n'est pas trop à recommander, mais dans l'absolu, c'est possible.

+0 -0

Merci, mais mon problème est essentiellement pour ajouter des Listener, pour pouvoir écouter mes Events.

Si je me base sur le système utiliser par bukkit (Plugin Minecraft), avant les méthode contenant un évenement on doit ajouter @EventHandler.

Un événement se présentait sous cette forme:

1
2
3
4
@EventHandler
public void uneMethodeQuelquonque(PlayerJoinEvent e){
   //Une action
}

Comment dois-je m'y prendre pour faire un système similaire ?

Soit en utilisant l'annotation processor tool de Java et en générant le code qui va bien lors de la compilation (c'est pas simple), soit en "scannant" les classes de l'utilisateur par réflexion (mais c'est pas génial). Implémenter une interface serait beaucoup plus facile.

Style : default void onNewPlayer(PlayerJoinEvent e) {}

+0 -0

Très bien, je ferai donc avec des interfaces.

Actuellement pour appeler mes méthodes je fais comme ceci:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    private void event(){
        mainApp.getPrimaryStage().widthProperty().addListener((observable, oldValue, newValue) -> {
            for(Class plugin : plugins){
                Method method = null;
                try{
                    method = plugin.getDeclaredMethod ("WindowWidthResizeEvent");
                    Object instance = plugin.newInstance ();
                    Object result = method.invoke (instance);
                }catch(NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e){
                    e.printStackTrace();
                }
            }
        });
    }

Le gros problème est que si je dois faire ça pour … tous les événements c'est un petit peu une usine à gaz. Une solution meilleure à proposer ?

Et si l'événement n'a pas été réécris, j'ai droit à une belle NoSuchMethodException

Pour moi il y a un problème de fond plus gênant dans ton code: tu devrais stocker des instances permanentes de tes différents plugins, et non pas juste leur classe. Parce que là tu instancies un nouvel objet à chaque fois qu'un évènement se produit. C'est assez lourd et plutôt limitant, tu ne peux pas créer de plugin stateful facilement.

Tu ne devrais faire appel à la réflexion pour créer une instance d'un plugin que lors de l'initialisation de l'application ou le démarrage du plugin en question. Par la suite tu conserves une instances permanente que tu réutilises.

Avec cette façon de faire et les implémentations par défaut des interfaces, ton code pourrait drastiquement se raccourcir en quelque chose comme :

1
2
3
4
5
6
    private void event(){ 
        mainApp.getPrimaryStage().widthProperty().addListener((observable, oldValue, newValue) -> { 
            for(Plugin plugin : plugins){ 
                    Object result = plugin.windowWidthResizeEvent();
            } 
        }); 
+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte