Dynamically loaded spring contexts

Viewed 2260

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

3 Answers

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.

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.

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.

Related