Instancier dynamiquement des objets via fichier de conf et injection

Une ferme de classifieur configurable

Le problème exposé dans ce sujet a été résolu.

Bonjour,

au boulot, on développe un logiciel qui classifie des site en fonction qu’il sont ou non des menaces style phishing/distributeur de malware ou pas.

Pour cela on a un process qui consiste à extraire des features puis à les passer à différents classifieurs.

par expérience, on sait que sur certains cas, un classifieur empirique sera meilleur (moins de FN et pas de FP) qu’un classifieur en machine learning mais dans beaucoup d’autres cas, le ML c’est plus efficace.

Du coup on a organisé notre système ainsi :

On considère une interface "Classifier" (qu’on appelé AnalysisFunction pour pas confondre les trucs empiriques et les ML) :

1
2
3
4
5
public abstract class AnalysisFunction {
    abstract public StatusType analyze(List<TokenEntity> tokens);
    abstract public double getPhishingProbability(List<TokenEntity> tokens);

}

A partir de là, je crée une "pool" de fonction d’analyse qui va se charger de choisir la fonction (l’algo n’est pas optimal mais bon) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PoolAnalysisFunction extends AnalysisFunction{
    private final List<AnalysisFunction> candidates;
    private final ChoiceFunction choice;
    private static final Logger LOG = LogManager.getLogger(PoolAnalysisFunction.class);

    public PoolAnalysisFunction(List<AnalysisFunction> candidates, ChoiceFunction choice) {
        this.candidates = candidates;
        this.choice = choice;
    }

    @Override
    public StatusType analyze(List<TokenEntity> tokens) {
        try {
            return choice.chooseAmong(candidates, tokens).analyze(tokens);
        } catch (ImpossibleChoiceException e){
            LOG.fatal("Not enough analysis function.", e);
            return StatusType.CLEAN;
        }
    }

    @Override
    public double getPhishingProbability(List<TokenEntity> tokens) {
        try {
            return choice.chooseAmong(candidates, tokens).getPhishingProbability(tokens);
        } catch (ImpossibleChoiceException e){
            LOG.fatal("Not enough analysis function.", e);
            return 0;
        }
    }
}
La pool de fonction, un "token" est un objet qui rassemble une feature, son label et quelques méta qu’on a enrichi grâce aux data de la boîte

Il s’avère que cette pool est totalement configurable : c’est à dire que je lui donne toutes les fonctions que je veux mettre du moment qu’elles sont dans le classpath.

Pour cela j’utilise une factory :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.glassfish.hk2.api.Factory;
public class PoolFunctionFactory implements Factory<AnalysisFunction> {
    private final PoolAnalysisParameters parameters;
    private static final Logger LOG = LogManager.getLogger(PoolAnalysisFunction.class);
    @Inject
    public PoolFunctionFactory(PoolAnalysisParameters parameters) {
        this.parameters = parameters;
    }

    @Override
    public AnalysisFunction provide() {
        try {

            Class<?> choice = Class.forName(parameters.getChoiceFunctionFQDN());
            ChoiceFunction choiceFunction = new PhishingPriorityChoiceFunction(); // default choice
            if(choice.getSuperclass().isInstance(ChoiceFunction.class)){
                choiceFunction = (ChoiceFunction) choice.newInstance();
            }
            List<AnalysisFunction> analysisFunctions = new LinkedList<>();
            // C'est ici que ça foire
            }
            return new PoolAnalysisFunction(analysisFunctions, choiceFunction);
        } catch (ClassNotFoundException|IllegalAccessException|InstantiationException e){
            LOG.fatal(e, e);
        }

        return null;
    }

    @Override
    public void dispose(AnalysisFunction analysisFunction) {
        LOG.trace(String.format("%s end of life", analysisFunction));
    }
}

`

En fait j’aimerai pouvoir instancier les fonctions à mettre dans la pool grâce à leur nom. Pour cela j’ai une clef style analysis.pool.functions=com.vadesecure.analysis.function.EmpiricalFunction1,com.vadesecure.analysis.function.MLFunction1.

Le problème c’est que ces objets on des dépendances : qui des propriétés pour configurer des seuils et des coefs, qui un modèle (oui, le ML se base sur des modèles). Cela j’aimerai pouvois l’injecter dynamiquement.

Prenons un exemple avec une fonction basée sur un SVM simple :

 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 SVMF2AnalysisFunction extends AnalysisFunction {
    private final SVMContainer modelContainer;
    private double probability = 0.0;
    private double threshold = 0.9;
    @Inject // ça, c'est un truc injecté car j'ai un process qui tourne en parallèle pour builder et mettre à jour le modèle
    public SVMF2AnalysisFunction(SVMContainer modelContainer) {
        this.modelContainer = modelContainer;
    }

    @Override
    public StatusType analyze(List<TokenEntity> tokens) {
        if (modelContainer.getModel() == null) {
            return null;
        }
        probability = modelContainer.getModel().analyse(tokens.stream());
        return probability >= threshold ? StatusType.PHISHING : StatusType.CLEAN;
    }

    @Override
    public double getPhishingProbability(List<TokenEntity> tokens) {
        return probability;
    }
}

Ma question est donc : comment faire pour pouvoir générer les différentes fonctions et y injecter les dépendances dans ma factory?

J’avais penser injecter le serviceLocator mais ça ne semble pas être une chose commune (j’ai pas trouvé de doc à ce propos). Un collègue m’a dit que les Proxy pouvaient aider mais je ne vois pas en quoi perso.

Avez-vous des idées.

Bonjour, avec l’aide des gens de stackoverflow et un peu de tests j’ai réussi à faire ce que je voulais.

L’idée de base est d’injecter dans ma PoolAnalysisFunctioNFactory un Iterable<AnalysisFunction> (pour plus de sécurité, j’utilise l’objet concret de HK2). A partir de là, j’ai la possibilité de choisir les objets que je prends en fonction de leur nom. Du coup il suffit, au moment de binder les classes de donner le nom adéquat et voilà.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
 * Generate the analysis pool
 */
public class PoolFunctionFactory implements Factory<AnalysisFunction> {
    private final PoolAnalysisParameters parameters;
    private static final Logger LOG = LogManager.getLogger(PoolAnalysisFunction.class);
    @Inject
    private IterableProvider<AnalysisFunction> registeredFunctions;
    @Inject
    public PoolFunctionFactory(PoolAnalysisParameters parameters) {
        this.parameters = parameters;
    }

    @Override
    public AnalysisFunction provide() {
        try {

            Class<?> choice = Class.forName(parameters.getChoiceFunctionFQDN());
            ChoiceFunction choiceFunction = new PhishingPriorityChoiceFunction(); // default choice
            if(choice.getSuperclass().isInstance(ChoiceFunction.class)){
                choiceFunction = (ChoiceFunction) choice.newInstance();
            }
            List<AnalysisFunction> analysisFunctions = new LinkedList<>();
            for(String candidatename : parameters.getAnalysisFunctionNames()){
                AnalysisFunction instance = registeredFunctions.named(candidatename).get();
                if(instance != null) {
                    analysisFunctions.add(instance);
                    Breadcrumbs.record(
                            new BreadcrumbBuilder().setLevel("INFO").setTimestamp(Date.from(Instant.now()))
                                    .setType("Analysis").setCategory("Log")
                                    .setMessage(String.format("Instanciated %s %s", candidatename, instance))
                                    .build()
                    );
                } else {
                    Breadcrumbs.record(
                            new BreadcrumbBuilder().setLevel("WARN").setTimestamp(Date.from(Instant.now()))
                                    .setType("Analysis").setCategory("Log")
                                    .setMessage(String.format("Could not instanciate %s (%s other candidates)",
                                            candidatename, registeredFunctions.getSize()))
                                    .build()
                    );
                    LOG.warn("Could not instanciate " + candidatename);
                }
            }
            return new PoolAnalysisFunction(analysisFunctions, choiceFunction);
        } catch (ClassNotFoundException|IllegalAccessException|InstantiationException e){
            LOG.fatal(e, e);
        }

        return null;
    }

    @Override
    public void dispose(AnalysisFunction analysisFunction) {
        LOG.trace(String.format("%s end of life", analysisFunction));
    }
}
La factory, avec en bonus des breadcrumps sentry
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class AnalysisFunctionBinder extends AbstractBinder{
    public static final Logger LOG = LogManager.getLogger(AnalysisFunctionBinder.class);
    @Override
    protected void configure() {
        LOG.info("passed in function binder");
        bindFactory(PoolFunctionFactory.class).to(AnalysisFunction.class).named("default").in(Singleton.class);
        bind(ProbabilityThresholdAnalysisFunction.class).named(ProbabilityThresholdAnalysisFunction.class.getName())
                .to(AnalysisFunction.class);
        bind(SVMF2AnalysisFunction.class).named(SVMF2AnalysisFunction.class.getName()).to(AnalysisFunction.class);

    }
}
Le binder
+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