如何在其他Java应用中启动无main方法的Spring Boot+Wicket8.0应用并实现插件化加载?
Got it, let's break down how to build this architecture step by step. I’ve worked on similar plugin-based systems before, so I’ll walk you through each component to replicate that Minecraft-style plugin loading while integrating Spring Boot, Spring Security, and Wicket 8.
Here's how the pieces fit together:
- Core Application: Acts as the plugin container, running Spring Boot + Spring Security + Wicket 8. It handles scanning, loading, and managing plugins.
- Plugins: Independent JAR files in the
/pluginsdirectory, each with aplugin.ymlmetadata file. Plugins must extend a base class and implement lifecycle methods (onEnable/onDisable). - Bootstrap: A way to start the core app without a dedicated
mainmethod (using Spring's application context).
First, create a base abstract class that all plugins must extend. This enforces the lifecycle methods and gives plugins access to the Spring context.
import org.springframework.context.ApplicationContext; public abstract class WicketPlugin { private ApplicationContext applicationContext; // Called when the plugin is loaded and ready public abstract void onEnable(); // Called when the core app is shutting down public abstract void onDisable(); // Inject Spring context (managed by core app) public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } // Helper to access Spring beans from the plugin protected ApplicationContext getApplicationContext() { return applicationContext; } }
Create a Spring-managed component that scans the /plugins directory, reads plugin.yml from each JAR, loads the plugin class, and triggers its lifecycle.
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import org.yaml.snakeyaml.Yaml; import java.io.File; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import java.util.Map; @Component public class PluginLoader { private static final Logger log = LoggerFactory.getLogger(PluginLoader.class); private static final String PLUGIN_DIR = "./plugins"; private final List<WicketPlugin> loadedPlugins = new ArrayList<>(); private final ApplicationContext applicationContext; public PluginLoader(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } // Scan and load all valid plugins public void loadPlugins() { File pluginDir = new File(PLUGIN_DIR); if (!pluginDir.exists() && !pluginDir.mkdirs()) { log.error("Failed to create /plugins directory"); return; } File[] pluginJars = pluginDir.listFiles((dir, name) -> name.endsWith(".jar")); if (pluginJars == null || pluginJars.length == 0) { log.info("No plugins found in {}", PLUGIN_DIR); return; } for (File jarFile : pluginJars) { loadSinglePlugin(jarFile); } } // Load a single plugin JAR private void loadSinglePlugin(File jarFile) { try (URLClassLoader classLoader = new URLClassLoader(new URL[]{jarFile.toURI().toURL()}, getClass().getClassLoader())) { // Read plugin.yml from the JAR root InputStream yamlStream = classLoader.getResourceAsStream("plugin.yml"); if (yamlStream == null) { log.error("Missing plugin.yml in {}", jarFile.getName()); return; } Yaml yaml = new Yaml(); Map<String, Object> pluginMeta = yaml.load(yamlStream); String mainClassName = (String) pluginMeta.get("main"); // Validate and instantiate the plugin class Class<?> pluginClass = classLoader.loadClass(mainClassName); if (!WicketPlugin.class.isAssignableFrom(pluginClass)) { log.error("Main class {} does not extend WicketPlugin", mainClassName); return; } WicketPlugin plugin = (WicketPlugin) pluginClass.getDeclaredConstructor().newInstance(); plugin.setApplicationContext(applicationContext); plugin.onEnable(); loadedPlugins.add(plugin); log.info("Loaded plugin: {} v{}", pluginMeta.getOrDefault("name", jarFile.getName()), pluginMeta.get("version")); } catch (Exception e) { log.error("Failed to load plugin {}", jarFile.getName(), e); } } // Clean up plugins on shutdown public void unloadPlugins() { loadedPlugins.forEach(plugin -> { try { plugin.onDisable(); } catch (Exception e) { log.error("Error disabling plugin", e); } }); loadedPlugins.clear(); } }
Configure Wicket to work with Spring, and set up the core app to start Wicket and plugins.
Wicket Spring Application Config
import org.apache.wicket.Page; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.spring.injection.annot.SpringComponentInjector; import org.springframework.context.ApplicationContext; public class WicketWebApp extends WebApplication { private final ApplicationContext springContext; public WicketWebApp(ApplicationContext springContext) { this.springContext = springContext; } @Override public Class<? extends Page> getHomePage() { return HomePage.class; // Replace with your home page } @Override protected void init() { super.init(); // Enable Spring dependency injection for Wicket components getComponentInstantiationListeners().add(new SpringComponentInjector(this, springContext)); // Mount pages (plugins can add more pages later) mountPage("/login", LoginPage.class); } }
Core Spring Boot Config (No Main Method)
import org.apache.wicket.protocol.http.WicketServlet; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @SpringBootConfiguration @EnableAutoConfiguration @EnableWebSecurity @EnableAspectJAutoProxy public class CoreApp { private final PluginLoader pluginLoader; public CoreApp(PluginLoader pluginLoader) { this.pluginLoader = pluginLoader; } @Bean public WebApplication wicketWebApp(org.springframework.context.ApplicationContext springContext) { return new WicketWebApp(springContext); } @Bean public ServletRegistrationBean<WicketServlet> wicketServletRegistration(WebApplication wicketApp) { WicketServlet servlet = new WicketServlet(); servlet.setApplication(wicketApp); ServletRegistrationBean<WicketServlet> registration = new ServletRegistrationBean<>(servlet, "/*"); registration.setLoadOnStartup(1); return registration; } // Load plugins after Spring context is ready @PostConstruct public void startPlugins() { pluginLoader.loadPlugins(); } // Unload plugins before shutting down @PreDestroy public void stopPlugins() { pluginLoader.unloadPlugins(); } }
Add a security config to protect your Wicket pages:
import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/") .permitAll() .and() .logout() .permitAll(); } // Example in-memory user store (replace with your auth system) @Bean @Override public UserDetailsService userDetailsService() { UserDetails user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } }
Plugin Jar Requirements
- Root file:
plugin.yml(required)name: ExamplePlugin version: 1.0.0 main: com.example.plugin.ExamplePluginMain - Main class: Must extend
WicketPluginimport org.apache.wicket.Page; import org.apache.wicket.protocol.http.WebApplication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ExamplePluginMain extends WicketPlugin { private static final Logger log = LoggerFactory.getLogger(ExamplePluginMain.class); @Override public void onEnable() { // Access Spring context and mount a custom page WebApplication wicketApp = getApplicationContext().getBean(WebApplication.class); wicketApp.mountPage("/example", ExamplePluginPage.class); log.info("ExamplePlugin enabled!"); } @Override public void onDisable() { log.info("ExamplePlugin disabled!"); } } - Package the plugin: Build a JAR with
plugin.ymlin the root, and all your plugin classes/resources. Drop it into the/pluginsdirectory of the core app.
Create a bootstrap class to initialize the Spring context from another application:
import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class AppBootstrap { public static void startCoreApp() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CoreApp.class); context.registerShutdownHook(); // Clean up on JVM exit } }
Call AppBootstrap.startCoreApp() from your external application to launch the entire system.
Add these to your core app's pom:
<!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Wicket 8 --> <dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket-spring-boot-starter</artifactId> <version>8.14.0</version> </dependency> <dependency> <groupId>org.apache.wicket</groupId> <artifactId>wicket-spring</artifactId> <version>8.14.0</version> </dependency> <!-- YAML parsing for plugin.yml --> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>2.0</version> </dependency>
内容的提问来源于stack exchange,提问作者ANGRY PENGUIN

