I have 2 Projects.
All the API contract (interfaces) defines in the demo-parent project (spring boot application)
The implementation for those defined in the demo-child project (spring boot application)
demo-parent is a dependency for demo-child, defined in the pom.xml of demo-child
In demo-parent :
AccessAPI.java
public interface AccessAPI {
void call();
}
SpringDemoParentApplication.java
#SpringBootApplication
public class SpringDemoParentApplication {
#Autowired
private AccessAPI accessAPI;
public static void main(String[] args) {
SpringApplication.run(SpringDemoParentApplication.class, args);
}
#PostConstruct
void exec() {
accessAPI.call();
}
}
In demo-child I have the implementation for the service :
AccessAPIImpl.java
#Service
public class AccessAPIImpl implements AccessAPI {
#Override
public void call() {
System.out.println("Executing from AccessAPIImpl");
}
}
Goal trying to achieve :
I must be able to build these projects independently, and pass the demo-child project jar via classpath to pick the implementation and inject all the implementation when running of demo-parent .
Such as :
>java -jar demo-parent.jar -cp demo-child.jar
I expected that the implementation would be picked up from the demo-child and autoinjected but It is not working as expected.
Note : I dont want to add demo-child dependency on demo-parent, the implementations/dependency must be picked up at runtime.
Please check the Git repository :
https://github.com/anthonyvk/spring-demo-child
https://github.com/anthonyvk/spring-demo-parent
TL; TR:
java -cp demo-parent.jar -Dloader.path=demo-child.jar org.springframework.boot.loader.PropertiesLauncher
if the demo-child.jar is a Spring Boot application, i.e. if the jar is repacked by the Spring Boot plugin, you need to change the loader.path:
java -cp demo-parent.jar -Dloader.path='demo-child.jar!/BOOT-INF/classes' org.springframework.boot.loader.PropertiesLauncher
Explanation:
By default, Spring Boot uses JarLauncher, which looks only in BOOT-INF/lib/ inside the jar archive, -cp argument has no effect. The commands above switch to PropertiesLauncher, which honors loader.path parameter (-cp equivalent). For details see The Executable Jar Format
By the way, the project structure seems to be a bit problematic:
adding spring boot application (demo-parent.jar) as a dependency to demo-child.jar is tricky, as demo-parent.jar is repacked by the Spring Boot plugin and maven cannot find AccessAPI class
there are runtime circular dependencies, even if not explicitly specified in the POM
I don't know detailed requirements, but if possible, I would suggest:
extracting interfaces to a separate project, let's say demo-api
making demo-parent and demo-child depended on demo-api, not on each other
making demo-api and demo-child regular jar's, not Spring Boot applications (removing Spring Boot plugin)
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.
I have some library jar lib.jar (made using spring boot but packaged as normal jar without spring boot plugin) which is made of spring boot and contains spring.components file generated by spring-context-indexer.
Now, I'm using this jar in my application which also has spring-context-indexer and its own spring.components file and uses some of the bean defined in lib.jar.
When I start my application, spring should register all beans defined in spring.components of lib.jar and spring.components of application. But spring isn't registering any of bean of lib.jar.
I tried using basePackages property of #SpringBootApplication but no results.
I even copied all entries of spring.components of lib.jar into spring.components of my application but no result.
Can anyone please help me?
TL;DR
If you're using Spring Data, #SpringBootApplication.scanBasePackages is not enough, you also need #EnableJdbcRepositories (or *Jpa* or whatsoever).
package application;
// without this annotation all Repository classes
// from library will be missing
#EnableJdbcRepositories({
"application",
"library"
})
#SpringBootApplication(
scanBasePackages = {
"application",
"library"
}
)
public class Application {
public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}
}
Some more info
Okay, maybe I'm a bit late, but I decided to investigate this case a bit.
That's what I've found as of 2 Feb 2022:
All META-INF/spring.components files are loaded in CandidateComponentsIndexLoader.doLoadIndex. You can use debug to check whether it sees file from lib or not
CandidateComponentsIndexLoader then creates CandidateComponentsIndex, which is then stored in the component scanner, for me it is AnnotationConfigServletWebServerApplicationContext.scanner.componentsIndex
Then in ClassPathScanningCandidateComponentProvider findCandidateComponents is called, which, if componentsIndex is not null, just gets components from that index by provided basePackage.
That's why missing basePackage is crucial.
I haven't dug into the Spring Data algorithms, but in my case Spring hadn't been generating library Repositories until I added the #EnableJdbcRepositories with packages.
P.S. All links represent files at the 5.3.15 tag, latest atm.
How do we load additional jar at runtime along with boot jar.
Primary jar: Main.jar
Additional jar: Support.jar
Main project is a gradle boot project.
Support project is NOT a gradle project but is given compile time dependencies to the required jars.
Contents of Support project:
#RestController
#RequestMapping("/test")
public class CustomService implements WebMvcConfigurer {
#RequestMapping(value = "/hello", method = RequestMethod.GET)
public #ResponseBody String get() {
return "Done!!";
}
}
What i tried:
java -cp Support.jar:Main.jar -Dloader.path=Support.jar -Xbootclasspath/p:alpn-boot-8.1.11.v20170118.jar -Dloader.main=com.abc.app.MyApplication org.springframework.boot.loader.PropertiesLauncher
The boot starts up fine but the endpoint is not registered.
NOTE:
I had mentioned annotation scanning.
#SpringBootApplication
#ComponentScan("com.abc")
public class MyApplication {
....
}
Also the Main.jar will be run from various places by various users. Each user might provide his own version of Support.jar. So, hardcoding the dependency into the gradle file of Main project is not feasible.
Try adding #ComponentScan(basePackages=full.name.of.customservice.package) to your spring application configuration, or make CustomService the same package as your #SpringApplication class
try using this - org.xeustechnologies.jcl.JarClassLoader from https://github.com/kamranzafar/JCL
JCL is a configurable, dynamic and extensible custom classloader that loads java classes directly from Jar files and other sources.
I am working on an application and deploying in cloud foundry. Internally it is having 3 custom dependencies developed by our team.
All 3 dependencies are boot project and have their #Configuration.
Dependency 1 is to interact with Couchbase. Source of this dependency is boot project.
Dependency 2 is to interact with FluentD for logging. Source of this dependency is boot project.
Dependency 3 is to interact with external rest service. Source of this dependency is boot project.
Dependency 4 is having all these above 3 dependencies and also having few utils classes and constants.
I am using this dependency 4 in multiple web applications which are having WebMVC implementation.
Everything is working fine in my local machine. But when I am pushing this web application on cloud, sometimes libraries getting executed before the web application which is crashing my app intermittently. Good thing app is getting recover in few seconds.
I did below changes in my libraries (jars/dependencies) and tried on cloud. After doing these changes ratio of app crash reduces, but unfortunately it is still crashing sometimes and I am able to see dependencies gets executed before application.
Added bootRepackage.enabled = false bootRepackage.withJarTask = jar in library's build.gradle
Took off from library and added in my web application
springBoot {
mainClass = "com.java.Application"
executable = true
}
Took off #SpringBootApplication from libraries(dependencies/jars). It's just in my web application now.
I do not know these are the only steps to make a boot dependency non-executable or I would have to do something else. Please let me know if I am missing something.
Here is the sample off application class of one of my dependency.
import org.springframework.context.annotation.ComponentScan;
#ComponentScan
public class LoggingApplication {
}
Sample of Web application main class.
#SpringBootApplication
#EnableWebMvc
#Import(LoggingApplication.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
You didn't expose stacktrace nor provided any specifics of "crash". So looking into my crystal ball, this sounds like you are doing some work during wiring phase of Spring IoC container. This "work" should be moved into #PostConstruct handlers, so that you are sure it will be executed after Spring Context is fully created and initialized.
I have an AWS lambda RequestHandler class which is invoked directly by AWS. Eventually I need to get it working with Spring Boot because I need it to be able to retrieve data from Spring Cloud configuration server.
The problem is that the code works if I run it locally from my own dev environment but fails to inject config values when deployed on AWS.
#Configuration
#EnableAutoConfiguration
#ComponentScan("my.package")
public class MyClass implements com.amazonaws.services.lambda.runtime.RequestHandler<I, O> {
public O handleRequest(I input, Context context) {
ApplicationContext applicationContext = new SpringApplicationBuilder()
.main(getClass())
.showBanner(false)
.web(false)
.sources(getClass())
.addCommandLineProperties(false)
.build()
.run();
log.info(applicationContext.getBean(SomeConfigClass.class).foo);
// prints cloud-injected value when running from local dev env
//
// prints "${path.to.value}" literal when running from AWS
// even though Spring Boot starts successfully without errors
}
}
#Configuration
public class SomeConfigClass {
#Value("${path.to.value}")
public String foo;
}
src/main/resources/bootstrap.yml:
spring:
application:
name: my_service
cloud:
config:
uri: http://my.server
failFast: true
profile: localdev
What have I tried:
using regular Spring MVC, but this doesn't have integration with #Value injection/Spring cloud.
using #PropertySource - but found out it doesn't support .yml files
verified to ensure the config server is serving requests to any IP address (there's no IP address filtering)
running curl to ensure the value is brought back
verified to ensure that .jar actually contains bootstrap.yml at jar root
verified to ensure that .jar actually contains Spring Boot classes. FWIW I'm using Maven shade plugin which packages the project into a fat .jar with all dependencies.
Note: AWS Lambda does not support environment variables and therefore I can not set anything like spring.application.name (neither as environment variable nor as -D parameter). Nor I can control the underlying classes which actually launch MyClass - this is completely transparent to the end user. I just package the jar and provide the entry point (class name), rest is taken care of.
Is there anything I could have missed? Any way I could debug this better?
After a bit of debugging I have determined that the issue is with using the Maven Shade plugin. Spring Boot looks in its autoconfigure jar for a META-INF/spring.factories jar see here for some information on this. In order to package a Spring Boot jar correctly you need to use the Spring Boot Maven Plugin and set it up to run during the maven repackage phase. The reason it works in your local IDE is because you are not running the Shade packaged jar. They do some special magic in their plugin to get things in the right spot that the Shade plugin is unaware of.
I was able to create some sample code that initially was not injecting values but works now that I used the correct plugin. See this GitHub repo to check out what I have done.
I did not connect it with Spring Cloud but now that the rest of the Spring Boot injection is working I think it should be straightforward.
As I mentioned in the comments you may want to consider just a simple REST call to get the cloud configuration and inject it yourself to save on the overhead of loading a Spring application with every request.
UPDATE: For Spring Boot 1.4.x you must provide this configuration in the Spring Boot plugin:
<configuration>
<layout>MODULE</layout>
</configuration>
If you do not then by default the new behavior of the plugin is to put all of your jars under BOOT-INF as the intent is for the jar to be executable and have the bootstrap process load it. I found this out while addressing adding a warning for the situation that was encountered here. See https://github.com/spring-projects/spring-boot/issues/5465 for reference.