Klassen instanzieren mit Spring

Codepfade

In einem Projekt das für Eingangsdaten sehr viele unterschiedliche Parser verwenden muss, ist es nur natürlich das wenn dieses in einer Klasse geschehen würde, dass das sehr unübersichtlich wird. Daher ist es besser, wenn man die Ganzheit in kleinere separate Parserklassen splittet.

Vor Spring

Nach diesem Schema bin ich in dem Projekt auch vorgegangen und habe für jede Gruppe einen eigenen Parser geschrieben. Alle Parser implementieren das Interface Parser, sodass sich folgender Code ergab:

//
// Instantiate class and execute parse
//
try {
    Class<?> c = Class.forName("de.pfau.parser.Parser00001");

    try {
        Parser p = (Parser) c.getDeclaredConstructor().newInstance();
        autowireCapableBeanFactory.autowireBean(p);

        p.parse();
    } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
        throw new Exception ...
    } 
} catch (ClassNotFoundException e) {
    log...
}

Da das Projekt noch aus einer Zeit stammt in der es noch nicht mit Spring kontakt hatte, kommt hier die newInstance() Methode zum Einsatz. Damit in den Klassen auch das Autowiring weiterhin funktioniert, muss nach der Verwendung von DI mit autowireCapableBeanFactory.autowireBean(p) Spring hierüber in Kenntnis gesetzt werden. Ansonsten haben die Autowired Felder null Werte.

Die Umsetzung mit Spring

Spring bringt hierfür Frist Class Support mit, da das grundlegend für das Dependency Injection ist.

Mit dem ClassPathScanningCandidateComponentProvider kann man den Classpath durchsuchen. Beim Instanzieren des Objektes kann ein Parameter useDefaultFilters angegeben werden. Setzt man ihn auf false, dann hat man eine leere Filterkette.

ClassPathScanningCandidateComponentProvider scanner =
        new ClassPathScanningCandidateComponentProvider(false);

Als nächstes fügen wir einen neuen Includefilter ein. Es gibt verschiedene Filter die eingefügt werden können (siehe TypeFilter). Für meine Zwecke eignet sich hier der RegexPatternTypeFilter. Als Parameter übergeben wir einen kompilierten RegEx Ausdruck, sodass Standard Java RegEx zum tragen kommt.

Die Klassen heißen alle ParserXXXXX. Wobei XXXXX für fünf stellige Dezimalzahl steht.

scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile("de.pfau.parser.*Parser\\d{5}")));

Über die Methode scanner.findCandidateComponents kann man eine gefilterte Liste der Kandidaten erhalten. Der Rest ist identisch zur alten Version.

Damit das Autowiring von autowireCapableBeanFactory funktioniert, darf der Code nicht im Konstruktor ausgeführt werden. Hierzu verwende ich die @PostConstruct Annotation. Die Instanzen der Parser werden in einer HashMap gespeichert, um so später darauf zuzugreifen.

ClassPathScanningCandidateComponentProvider scanner =
        new ClassPathScanningCandidateComponentProvider(false);

scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile("de.pfau.parser.*Parser\\d{5}")));

for (BeanDefinition bd : scanner.findCandidateComponents("de.pfau.parser")) {
    //
    // Instantiate parser and parse loaded document
    //
    try {
        var classname = bd.getBeanClassName();
        Class<?> c = Class.forName(classname);

        try {
            Parser p = (Parser) c.getDeclaredConstructor().newInstance();
            autowireCapableBeanFactory.autowireBean(p);
            map.put(classname, p);

            log.info("parser " + classname + " successfuly loaded");
        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
            throw new Exception...
        } 
    } catch (ClassNotFoundException e) {
        log.severe("could not load class");
    }
}