2

So I have a project in which I'm using Spring boot, and want to utilize a module system. I want the module system to be dynamically reloadable. I have it nearly working but @ComponentScan completely does not work in the modules.

There is a module folder containing jar files which are loaded at startup, and need to be dynamically unloaded, loaded, and reloaded.

The modules are created via AnnotationConfigApplicationContext, the context's classloader is set to the core's classloader, and a @Configuration class supplied by a method in the module interface is registered via context.register.

The @Bean declarations in the @Configuration work for autowire, and all the classes load properly. The problem is @ComponentScan doesn't work. I even tried annotating a class with @Component that has no field or anything, and it throws an autowire error like it's not in the context. This wouldn't be such a big deal if it wasn't also an issue with my repositories.

If there's a way I can manually run componentscan without the annotations I'm okay with doing that bit of extra work.

I've tried using the spring boot package with @SpringBootApplication(scanPackages = {"package.name"}

@EnableJpaRepository @EntityScan

are all in the appropriate places, and have correct parameters.

I tried using .register with the core context which didn't work. I can't really use that anyways, as you can de-register a configuration as far as I can tell.

This is my code that actually loads the jar file

    public Module loadModule(Path path) throws Exception {
    if (!path.toFile().exists()) {
        throw new Exception(String.format("Could not find module at %s", path.toAbsolutePath()));
    }

    JarFile file = new JarFile(path.toFile());
    ZipEntry moduleJson = file.getEntry("module.json");
    System.out.println(path.toUri().toURL());

    if (moduleJson == null) {
        throw new Exception(String.format("Could not find module json at %s", path.toAbsolutePath()));
    }

    String moduleJsonSTR = CharStreams
            .toString(new InputStreamReader(file.getInputStream(moduleJson), Charsets.UTF_8));
    Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .setPrettyPrinting().create();

    PluginConfig config = gson.fromJson(moduleJsonSTR,   PluginConfig.class);


    URLClassLoader classLoader = new URLClassLoader(new URL[] { path.toUri().toURL() },
            getClass().getClassLoader());
    Class mainClass = classLoader.loadClass(config.getMain());

    Enumeration<JarEntry> entries = file.entries();

    while (entries.hasMoreElements()) {
        JarEntry clazz = entries.nextElement();

        if (clazz.getName().equals(mainClass.getName())) {
            System.out.println("FOUND MAIN AND SKIPPING");
            continue;
        }

        if (clazz.isDirectory() || !clazz.getName().endsWith(".class")) {
            continue;
        }
        String className = clazz.getName().substring(0, clazz.getName().length() - 6);
        className = className.replace('/', '.');
        classLoader.loadClass(className);
    }
    Module module = (Module) mainClass.newInstance();
    System.out.println(module.getConfigurationClass().getName());
    module.setName(config.getName());
    file.close();
    classLoader.close();
    return module;
}

This is my code that initializes the context

public AnnotationConfigApplicationContext initializeContext() {
    SpringConfig cfg = getSpringConfig();
    this.context = new AnnotationConfigApplicationContext();
    this.context.setClassLoader(Core.class.getClassLoader());
    this.context.refresh();
    this.context.register(getConfigurationClass());
    Map<String, Object> props = context.getEnvironment().getSystemProperties();
    cfg.getProperties().forEach((key, value) -> props.put(key, value));
    return context;
}

There's a bean that is a blank class with @Component being autowired into a class in which the context is autowired without problem. So I know autowire is working, I know it's spring managed, but @ComponentScan isn't working.

I'd like to either get ComponentScan working, or find a way to manually add the components programmatically.

More code:

Core Plugin: This is my module class: https://hastebin.com/potayuqali.java This is the controller which loads said modules: https://hastebin.com/ipegaqojiv.java Example Module:

This is an example of one of the modules: https://hastebin.com/umujiqepob.java This is the @configuration for that module: https://hastebin.com/lakemucifo.css

Redmancometh
  • 76
  • 1
  • 11
  • Osgi is more suited for dynamic loading. see : https://dzone.com/articles/osgi-and-spring-dynamic – pdem Jul 04 '19 at 10:31

3 Answers3

1

If I understand correctly you try to load jar and register as a bean after ApplicationContext load. But ComponentScan scans @Component like annotation when the application context load not after. So you should register classes programmatically(as you do context.register(..)).

And after register classes maybe you have @Configuration annotated class and it has a @Bean annotated methods. To register that @Bean annotated methods you can run

@Autowire
ConfigurationClassPostProcessor configurationClassPostProcessor;

and

configurationClassPostProcessor.processConfigBeanDefinitions(applicationContext.getDefaultListableBeanFactory());

Also you can look at my answer: Is There Any Way To Scan All @Component like Annotated Class After Spring ApplicationContext Load

Another solution:

To scan base packages(like @ComponentScan) use

annotationConfigApplicationContext.scan(basePackageName).

It scan base packages and register @Component(etc) annotated classes as @ComponentScan do. But classLoader that loads jar must be the same with annotationConfigApplicationContext#classLoader. So you must set classLoader with:

annotationConfigApplicationContext.setClassLoader(yourClassLoader)

or else AnnotationConfigApplicationContext cannot find and register classes.

  • I am actually loading jars from the spring application, with each JAR deploying its own context. Here is the controller code for loading the jars: https://github.com/Redmancometh/StormCore/blob/master/src/main/java/org/stormrealms/stormcore/controller/ModuleLoaderController.java Here is the master plugin interface: https://github.com/Redmancometh/StormCore/blob/master/src/main/java/org/stormrealms/stormcore/StormPlugin.java – Redmancometh Jul 05 '19 at 02:45
  • I cannot understand what meaning 'with each JAR deploying its own context' but I updated my post. – Burak Helvacı Jul 08 '19 at 14:06
  • As in each jar has it's own context. If you look in the StormPlugin class you can see the initializeContext plugin which is called on every plugin load. Each plugin is its own SpringApplication, and has it's own ApplicationContext. So it's not so much after ApplicationContext load, as I don't want to add it to the StormCore context. I want each plugin to establish its own context if that makes sense. – Redmancometh Jul 08 '19 at 17:47
1

Unfortunately none of your code links are working, so it's a bit difficult to understand why the Components for the Module dont get registered.

But I do know that Spring gets difficult when you don't let it manage the bean lifecycle.

It might be too big a change to your app, but instead of trying to write my own plugin system, but I would try out the Plugin Framework for Java. It's a fairly typical plugin framework, and it's got a spring plugin as well.

Basically you write your plugins, build them as zip/jar and drop them into a plugins folder.

stringy05
  • 6,511
  • 32
  • 38
  • Apologies here is a link to the project: https://github.com/Redmancometh/StormCore/ Here is a link to the controller loading up the modules: https://github.com/Redmancometh/StormCore/blob/master/src/main/java/org/stormrealms/stormcore/controller/ModuleLoaderController.java Here is the plugin abstract: https://github.com/Redmancometh/StormCore/blob/master/src/main/java/org/stormrealms/stormcore/StormPlugin.java I'll try that plugin system though it might work for my needs. – Redmancometh Jul 05 '19 at 02:46
0

Any chance that the @SpringBootApplication class is in the root package of one of basePackages of @ComponentScan? For some weird reason, for which I am still looking for an explanation, moving my SpringBootApplication class from com.comp.app to com.comp.app.xyx, loaded all components from com.comp.app.* that werelocated in other dependencies.

  • Sadly no. It's not a spring boot application it's just a plain old spring application utilizing @Configuration. For the ComponentScan I've even tried specifying baseClasses to no avail. – Redmancometh Jul 05 '19 at 02:43