My project uses springboot, after pack it to jar file, I can execute java -jar my-project.jar to run the instance.
I also have a script which is a main class, when I execute java -cp my-project.jar com.test.MyScript, it says Could not find or load main class.
How can this happen? By the way, when I execute java -cp my-project.jar com.test.MyApplication, the same error occurs. MyApplication is the SpringBootApplication class.
Why can't I manually run the main class?
That is because spring boot doesn't use the SpringBootApplication as the main entry point. It uses org.springframework.boot.loader.Launcher.
Excerpt from the spring docs:
The org.springframework.boot.loader.Launcher class is a special
bootstrap class that is used as an executable jar’s main entry point.
It is the actual Main-Class in your jar file, and it is used to setup
an appropriate URLClassLoader and ultimately call your main() method.
Related
I have a Spring Boot application which copies external JAR files to a folder, depending on certain conditions. These JARs can contain many Spring components (i.e. classes annotated or meta-annotated with #Component) and the Spring application should be able scan and instantiate for these beans. Is it possible, based on certain conditions, to dynamically load the contents of the JAR files and make them available to the Spring application context? I am fully aware of the security implications this has.
I have read about the different types of Launchers which Spring provides for its executable JAR format, such as JarLauncher and PropertiesLauncher, but it looks like that these launchers do not detect changes to the classpath, but instead only scan the directories once for JAR files.
The following simple application demonstrates the problem:
// .../Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
System.out.println("Please copy JAR files and press Enter ...");
System.in.read();
SpringApplication.run(Application.class, args);
}
}
Replace the default JarLauncher with PropertiesLauncher:
// build.gradle
tasks.named('bootJar') {
manifest {
attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher',
'Start-Class': 'com.example.customlauncher.Application'
}
}
Specify the location to the external JARs in the properties file of the PropertiesLauncher:
# .../resources/loader.properties
loader.path=file:/path/to/dir
The application is a Spring Initializer Gradle application and packaged by running the bootJar task: ./gradlew bootJar.
It is then started with the following command:
java -jar build/libs/customlauncher-0.0.1-SNAPSHOT.jar
This works if the JAR file is already present at the specified location (/path/to/dir), but it does not work if the java command is executed while the directory is empty and the JAR file is then copied while the app waits for the user to copy the files and press Enter ↲.
There are a couple of related questions, but it looks like they all assume that the JAR files already exist at the time of starting the JVM:
How to put a directory first on the classpath with Spring Boot?
Spring Boot Executable Jar with Classpath
SpringBoot external jar not load
Is there a way to achieve this without too many awkard hacks? Or is recommended to utilize something like OSGi? Am I looking at this completely wrong and there is a better way to have JARs on the classpath that do not need always need loading (if the JAR is "disabled", it should not be loaded/compiled by the JVM, should not be picked up by Spring, etc.)?
It looks like this is possible if the JAR files are copied before starting the Spring application. It feels hackish, but it works. Use at your own risk!
You need two classes, one for bootstrapping the external JARs, which will then start the second via a manually created PropertiesLauncher. The bootstrapping class can be a plain old regular Java class (but it can be a Spring Boot Application too) and only the second class needs to be a SpringBootApplication.
// BootstrapApplication.java
public class BootstrapApplication {
public static void main(String[] args) {
System.out.println("Please copy JAR files and press Enter ...");
System.in.read();
PropertiesLauncher.main(args);
}
}
// Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
In the gradle file, we can switch back to the default JarLauncher, by removing the bootJar task manifest configuration and applying settings via the springBoot configuration block. mainClass will end up as Start-Class in the MANIFEST.MF file.
// build.gradle
springBoot {
mainClass = 'com.example.customlauncher.BootstrapApplication'
}
In the properties file for the loader, a new property needs to be set, which points to the real application class. The settings in this file are only picked up by PropertiesLauncher and ignored by JarLauncher. In other words: JarLauncher delegates to Start-Class from the manifest file and PropertiesLauncher delegates to loader.main from its properties file.
# .../resources/loader.properties
loader.path=file:/path/to/dir
loader.main=com.example.customlauncher.Application
Spring (Boot) will first call the main method of BootstrapApplication, as specified in the MANIFEST.MF file (controlled via springBoot configuration block in the build.gradle file). In the implementation of this main method, a new PropertiesLauncher is created with the main class set to the "real" application (i.e. Application).
Executing the application is still done via the same invocation:
java -jar build/libs/customlauncher-0.0.1-SNAPSHOT.jar
Any JAR files added to /path/to/dir after the JVM has started, but before calling PropertiesLauncher#main in BootstrapApplication are then available in the classpath and application context as seen from Application.
In a module structure like this:
project
|
|- common module
|- app module
Where app module has common module as a dependency, I have a custom classloader class defined in the common module. The app module has a -Djava.system.class.loader=org.project.common.CustomClassLoader jvm parameter set to use that custom classloader defined in common module.
Running a spring boot project within IDEA this works perfectly. The custom classloader is found, set as a system classloader and everything works.
Compiling a runnable jar (using default spring-boot-maven-plugin without any custom properties), the jar itself has all the classes and within it's lib directory is the common jar which has the custom classloader. However running the jar with the -Djava.system.class.loader=org.project.common.CustomClassLoader results in the following exception
java.lang.Error: org.project.common.CustomClassLoader
at java.lang.ClassLoader.initSystemClassLoader(java.base#12.0.2/ClassLoader.java:1989)
at java.lang.System.initPhase3(java.base#12.0.2/System.java:2132)
Caused by: java.lang.ClassNotFoundException: org.project.common.CustomClassLoader
at jdk.internal.loader.BuiltinClassLoader.loadClass(java.base#12.0.2/BuiltinClassLoader.java:583)
at jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(java.base#12.0.2/ClassLoaders.java:178)
at java.lang.ClassLoader.loadClass(java.base#12.0.2/ClassLoader.java:521)
at java.lang.Class.forName0(java.base#12.0.2/Native Method)
at java.lang.Class.forName(java.base#12.0.2/Class.java:415)
at java.lang.ClassLoader.initSystemClassLoader(java.base#12.0.2/ClassLoader.java:1975)
at java.lang.System.initPhase3(java.base#12.0.2/System.java:2132)
Why does this happen? Is it because in the runnable jar the classloader class is in a jar in lib directory so the classloader is trying to get set before the lib classes were added to the classpath? Is there anything I can do besides moving the classloader from common to all the other modules that need it?
EDIT: I've tried moving the custom classloader class from common module to app but I am still getting the same error. What is going on here?
Running a spring boot project within IDEA this works perfectly. The custom classloader is found, set as a system classloader and everything works.
Because IDEA puts your modules on the class path and one of them contains the custom class loader.
Is it because in the runnable jar the classloader class is in a jar in lib directory so the classloader is trying to get set before the lib classes were added to the classpath?
Kind of. The lib classes are not "added to the class path", but the runnable Spring Boot app's own custom class loader knows where to find and how to load them.
For a deeper understanding of java.system.class.loader, please read the Javadoc for ClassLoader.getSystemClassLoader() (slightly reformatted with added enumeration):
If the system property java.system.class.loader is defined when this method is first invoked then the value of that property is taken to be the name of a class that will be returned as the system class loader.
The class is loaded using the default system class loader and must define a public constructor that takes a single parameter of type ClassLoader which is used as the delegation parent.
An instance is then created using this constructor with the default system class loader as the parameter.
The resulting class loader is defined to be the system class loader.
During construction, the class loader should take great care to avoid calling getSystemClassLoader(). If circular initialization of the system class loader is detected then an IllegalStateException is thrown.
The decisive factor here is #3: The user-defined system class loader is loaded by the default system class loader. The latter of course has no clue about how to load something from a nested JAR. Only later, after the JVM is fully initialised and Spring Boot's special application class loader kicks in, can those nested JARs be read.
I.e. you are having a chicken vs. egg problem here: In order to find your custom class loader during JVM initialisation, you would need to use the Spring Boot runnable JAR class loader which has not been initialised yet.
If you want to know how what the Javadoc above describes is done in practice, take a look at the OpenJDK source code of ClassLoader.initSystemClassLoader().
Is there anything I can do besides moving the classloader from common to all the other modules that need it?
Even that would not help if you insist in using the runnable JAR. What you could do is either of these:
Run your application without zipping it up into a runnable JAR, but as a normal Java application with all application modules (especially the one containing the custom class loader) on the class path.
Extract your custom class loader into a separate module outside of the runnable JAR and put it on the class path when running the runnable JAR.
Set your custom class loader via Thread.setContextClassLoader() or so instead of trying to use it as a system class loader, if that would be a viable option.
Update 2020-10-28: In the document "The Executable Jar Format" I found this under "Executable Jar Restrictions":
System classLoader: Launched applications should use Thread.getContextClassLoader() when loading classes (most libraries and frameworks do so by default). Trying to load nested jar classes with ClassLoader.getSystemClassLoader() fails. java.util.Logging always uses the system classloader. For this reason, you should consider a different logging implementation.
This confirms what I wrote above, especially my last bullet point about using the thread context class loader.
Assuming you want to add custom jar to the classpath with Spring, do the following:
Generate the jar file with the maven jar plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
<mainClass>
com.demo.DemoApplication
</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
While running the application from the command line, use the below command
java -cp target/demo-0.0.1-SNAPSHOT.jar -Dloader.path=<Path to the Custom Jar file> org.springframework.boot.loader.PropertiesLauncher
This should launch your app while loading the Custom Classloader as well
In short, the trick is, to use the -Dloader.path along with org.springframework.boot.loader.PropertiesLauncher
An application launched on the lines of -
java -cp ./lib/* com.example.Main
would ideally be sufficient.
Will need some clarity on how the application is being used.
Is the main class itself being attempted to be launched from a custom class loader (assuming its possible to do so) or whether post launch specific application related classes are required to be loaded with a custom class-loader (and associated privileges)?
Have asked those questions in the comments above (planning to update the answers here once have more clarity).
PS: Haven't really factored the use of 'modules' yet but believe the above syntax would still hold for the newer jdk's (after jdk 8).
I am trying out the various access rules about who can access and what and I saw this statement in The State of the module system document,
The unnamed module reads every other module. Code in any type loaded from the class path will thus be able to access the exported types of all other readable modules, which by default will include all of the named, built-in platform modules.
So, I wrote the following code to test it out with the following structure:
moduleA/modA.A --> automod/automod.Foo --> nonmodular.Junk --> moduleX/modX.X
Basically,
moduleA's modA.A calls a method on a non-modular class automod.Foo. automod.Foo is packaged into automod.jar and put on the module-path. module-info for moduleA has requires automod; clause. This works fine, as expected.
automod.Foo calls a method on nonmodular.Junk class. nonmodular.Junk is packaged into nonmodular.jar and put on classpath. This works fine, as expected.
nonmodular.Junk calls a method on moduleX's modX.X. modX.X is packaged into moduleX.jar.
It is this step that has a problem. It works if I put moduleX.jar on classpath but not if I put moduleX.jar on module-path. (module-info for moduleX does have exports modX; clause.)
In other words, the following command works:
java --module-path moduleA.jar;automod.jar; -classpath nonmodular.jar;moduleX.jar --module moduleA/modA.A
With the following output:
In modA.A.main() Calling automod.Foo()
In automod.Foo()
In modA.A.main() Calling automod.foo.main()
In automod.Foo.main() Calling nonmodular.Junk()
In automod.Foo.main() Calling nonmodular.Junk.main()
In nonmodular.Junk.main calling new modX.X()
In modX.X()
But the following command doesn't work:
java --module-path moduleA.jar;automod.jar;moduleX.jar -classpath nonmodular.jar; --module moduleA/modA.A
Here is the output:
In modA.A.main() Calling automod.Foo()
In automod.Foo()
In modA.A.main() Calling automod.foo.main()
In automod.Foo.main() Calling nonmodular.Junk()
In automod.Foo.main() Calling nonmodular.Junk.main()
In nonmodular.Junk.main calling new modX.X()
Exception in thread "main" java.lang.NoClassDefFoundError: modX/X
at nonmodular.Junk.main(Junk.java:5)
at automod/automod.Foo.main(Foo.java:10)
at moduleA/modA.A.main(A.java:10)
Caused by: java.lang.ClassNotFoundException: modX.X
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 3 more
Any idea why? Any class loaded from the classpath should be able to access any classes exported by a module.
When you start a Java application with the --module command, the value you pass is a "root" module. The same is true of modules added via --add-modules. The module system determines the entire module graph from these root modules. In other words, it reads the module-info file, finds the requires directives, and then searches the modulepath for those required modules. It does this transitively. Some modules also declare one or more uses directives on a service. Any modules on the modulepath that provides any of those services will also be loaded, regardless of if any module requires them.
This means if there's a module on the modulepath that isn't required by any loaded module and doesn't provide any services needed by any loaded module then said module won't be loaded. If you're interested in seeing what modules are resolved you can use the following command:
java --show-module-resolution --dry-run -p [MODULEPATH] -m [MODULE]
In your case I can only assume that none of your other modules require modularX, so when its on the modulepath it doesn't get loaded. However, when its on the classpath things work differently and its found by your non-modular code that's also on the classpath. You can still use the modulepath though, just make sure your moduleX module is loaded. This can be forced by using --add-modules:
java -p moduleA.jar;automod.jar;moduleX.jar --add-modules moduleX -cp nonmodular.jar -m moduleA/modA.A
Note you can also limit the modules via --limit-modules.
i want to use Spring Boot with Spring Security and Apache Wicket 8.0 as web view.
It`s easy to do it with SpringBoot and just simply run jar file, but i want to do something like this:
Minecraft Server (Spigot, Bukkit, etc..) Scanning /plugins/ folder, and looking for .jar files.
All files must contain plugin.yml in root of .jar archive. This plugin.yml file contains a path to class file, for example - BukkitMain and this class MUST extend JavaPlugin.
Then MinecraftServer core is executing onEnable() method of BukkitMain.class (so, if we override this method - server code will execute all code inside this method)
For Example:
public class BukkitMain extends JavaPlugin {
#Override
public void onEnable(){
//SpringApplicationBuilder.run(....);
}
}
So, i can use Spring-boot-plugin in build part of maven .pom file, but it's packing all class files and resources to BOOT-INF inside jar and it's requiring main(String args[]) method to run SpringApplication. It`s bad variant, because i can't access my BukkitMain to run it.
When i use Maven Compiler Plugin and packing it to jar - it's all ok, but there is new problem. My BukkitMain trying to run SpringApplicationBuilder, but i'm getting ClassNotFoundException, cause there is no SpringApplicationBuilder inside jar.
I am using IntelliJ and jus checked out working code from the svn.I am struggling to run the jar.
Its a simple core java Spring project.
Since I get the above error.I understand that the spring path is not set fine.
How do I handle it.?
private ApplicationContext appContext = new AnnotationConfigApplicationContext(ApplicationRepositoryConfiguration.class);
Application context file is a bean class here (#Bean annotation is used). I am not using a xml file.
It seems like you are running your project from command line. Run following command :
java -classpath spring.jar;spring-sec.jar,......so on com.example.UrMainClass
while specifying jar in command, make sure you provide complete path of jar. Also check this link https://stackoverflow.com/a/10122038/1065180