How to force the import/loading of rest controller programatically - java

I'm starting a spring boot rest service which may load different packages depending on the distribution. This means sometimes the distribution will contain some jars where certain REST controllers are, sometimes this controllers are not there.
So How I'm able to tell spring-boot where to find the controllers with a configuration files. Now I'm sending this info by annotations forcing me to create a "main" per distribution. I will like to define a unique main that imports the controllers defined in a file. In other words I want to access the #Importannotation manually as is shown in the sniped bellow:
#Configuration
#PropertySource("conf.cfg")
#Import(value = {RestContorller1.class, RestContorller2.class})
#EnableAutoConfiguration
#ConfigurationProperties
#SpringBootApplication
#RestController
#EnableSwagger2
public class Application {
public static void main(String[] args) {
String confFile = Const.DEFAULT_CONFIGURATION_FILE;
if(args.length>0)
confFile= args[0];
System.setProperty("spring.config.name",confFile);
Boolean hasStarted = DataProcessingCore.start(confFile);
if(hasStarted) {
SpringApplication springApp = new SpringApplication(Application.class);
try {
springApp.setDefaultProperties(Utils.createPropertyFiles(confFile));
} catch (IOException e) {
e.printStackTrace();
}
springApp.addInitializers();
springApp.run(args);
}
}
}

If I understand you corretly, your controllers reside in a JAR imported by maven/gradle to your main project/s.
In order to create auto-configuration like spring boot does, that in the same way can be used to import your controllers when the jar is in the classpath, you can tell spring to find your custom configuration on start-up.
I wrote a simple example for that here: Creating your own auto-configuration
In pricipal, you create a spring-boot application (without the spring boot maven plugin!! it is important for the classpath and packaging). and create a file named spring.factories (you can find the actual content in the guide I linked) that tells any spring-boot application that have this jar to load your configuration that may do a #ComponentScan to search for your controllers or set the #Bean manually.
Doing that, you don't have to do #Import and the controllers will be loaded dynamically.

Related

SpringApplicationBuilder parent webapp and child without webapp doesn't work

I have a spring boot app with a parent application context and a child application context with some legacy code. I would like to have a webapp in the parent application context, and have the child context be a non webapp.
I have my config and properties in application.yml, that gets loaded in the main class
#ComponentScan(...)
#PropertySource(value = "classpath:application.yml", factory = YamlPropertySourceFactory.class)
#SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MyApplication(
public static void main(String[] args) {
new SpringApplicationBuilder(MyApplication.class)
.profiles("Prod")
.web(WebApplicationType.SERVLET)
.listeners(new MainAppListener()) // logging for some of the events
.child(LegacyServiceConfig.class)
.web(WebApplicationType.NONE)
.listeners(new LegacyAppListener()) // logging for some of the events
.run(args);
}
}
What I've tried so far:
I don't have any web application type config in my application.yml (spring.main.web-application-type). I hoped that the configuration I've specified thru code, in the above snippet would have worked, but it doesn't. Neither of the application contexts start as a webapp, effectively ignoring the config web(WebApplicationType.SERVLET) in the above snippet.
I tried putting spring.main.web-application-type=servlet in my
application.yaml. But that seems to apply this config to both parent
and child application context. And both of them try to start as
webapps, which is not what I want.
How do I get this to work? Any pointers?
As per my knowledge in spring-boot start from main only and the main class is provided by Maven, because I had worked on Maven project. All the properties in the project like JDBC connection, port change you can do it in .properties(extension) type file. And this file can also be in format of .yml also. These file is totally different from main class.
If you make any changes in main or add any extra annotation in main then it will throw an error.
So keep every properties in .properties or .yml file and all configuration component scan in pom.xml file you can have two .properties file also
But main class should be simple like this:
#SpringBootApplication()
public class SpringBootProjectApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootProjectApplication.class, args);
}
}
Hope this will help.

How to inject extra property sources in SPRING before other beans are created?

I am writing a library which will be used by spring-boot projects. I'd like to inject into the boot projects' SpringEnvironment a property source that I take from the Internet.
I tried the following
#Configuration
public class MyCustomConfiguration {
#Bean
BeanDefinedAbove above() { /* do some work */ }
#Bean
#ConditionalOnBean(BeanDefinedAbove.class)
SmartInitializingSingleton propertySourceSetting(ConfigurableEnvironment env, BeanDefinedAbove bean) {
return () -> {
PropertySource source = bean.getPropertySourceDownloadedFromTheInternet();
env.getPropertySources().addFirst(source);
}
}
}
In my clients' projects when I debug this code what happens is either one of the two:
above() is called
user's #Service or #Controller are called
propertySourceSetting(...) is called
OR
user's #Service or #Controller are called
above() is called
propertySourceSetting(...) is called
Depending whether or not my client's depend on my BeanDefinedAbove bean, which is normal as the #Service is depdent on the bean created in above().
I have also added the FQDN of my class to the EnableAutoConfiguration in the META-INF/spring.factories.
So how to ensure that the logic in propertySourceSetting(..) is called before users' #Service and #Controller
I'll provide you with three options.
Option 1: (THIS IS A BAD APPROACH, BUT A QUICK WORKAROUND)
Add #Lazy(false) annotation to both Beans. Spring will eagerly create those two beans, which they will probably be created before the other ones.
Why this is bad?
This does not ensure order. Spring decides the creation order based on dependencies and some other conditions. This is why it will "probably" work :)
Option 2: Create a main class to bootstrap Spring Boot Initialization (the old way of starting spring boot).
public static void main(String[] args) throws Exception {
SpringApplication application = new SpringApplication(MyApplication.class);
// ... add property source here before start
application.run(args)
}
You also need to specify main class in the manifest for Spring Boot like this: https://www.baeldung.com/spring-boot-main-class
In that main-class you would add your propertysource, kinda like this:
SomeClassThatRetrievesProperties propRetriever = new SomeClassThatRetrievesProperties ();
Map<String,String> properties = propRetriever.getAllPropertiesAsMap();
application.setDefaultProperties(properties);
Option 3: Create a CustomApplicationContext by extending WebApplicationContext and overriding getSpecificConfigurations() method.
This way you will have full control but we aware that you could break some important stuff.

Spring Config server add property at runtime

I want to add some property at runtime in spring config server and it should be available to all client applications with #Value annotation.
I wont have this property predefine because I am going calculate that value in spring config server and add to environment.
Can you please help me understand what is best way to achieve this.
Spring cloud configuration contains a feature named 'RefreshScope' which allows to refresh properties and beans of a running application.
If you read about spring cloud config, it looks like it can only load properties from a git repository, but that is not true.
You can use RefreshScope to reload properties from a local file without any need to connect to an external git repository or HTTP requests.
Create a file bootstrap.properties with this content:
# false: spring cloud config will not try to connect to a git repository
spring.cloud.config.enabled=false
# let the location point to the file with the reloadable properties
reloadable-properties.location=file:/config/defaults/reloadable.properties
Create a file reloadable.properties at the location you defined above.
You can leave it empty, or add some properties. In this file you can later, at runtime, change or add properties.
Add a dependency to
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
All beans, that are using properties, that may be changed during runtime, should be annotated with #RefreshScope like this:
#Bean
#RefreshScope
Controller controller() {
return new Controller();
}
Create a class
public class ReloadablePropertySourceLocator implements PropertySourceLocator
{
private final String location;
public ReloadablePropertySourceLocator(
#Value("${reloadable-properties.location}") String location) {
this.location = location;
}
/**
* must create a new instance of the property source on every call
*/
#Override
public PropertySource<?> locate(Environment environment) {
try {
return new ResourcePropertySource(location);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Configure Spring to bootstrap the configuration using that class.
Create (or extend) the META-INF/spring.factories file in your resource folder:
org.springframework.cloud.bootstrap.BootstrapConfiguration=your.package.ReloadablePropertySourceLocator
This bean will read the properties from the reloadable.properties. Spring Cloud Config will reload it from disk, when you refresh the application.
Add runtime, edit reloadable.properties as you like, then refresh the spring context.
You can do that by sending a POST request to the /refresh endpoint, or in Java by using ContextRefresher:
#Autowired
ContextRefresher contextRefresher;
...
contextRefresher.refresh();
This should also work, if you choose to use it in parallel to properties from a remote git repository.

Spring Boot Externalizing properties not working

I have looked at the below threads and followed things given there. Still my property override is not happening
Spring Boot - Externalized properties
Profile Specific Property Enablement
Spring Boot External Config
I am on Tomcat 8.0.33 and Spring boot starter web and got this in my setenv.sh
export JAVA_OPTS="$JAVA_OPTS -Dlog.level=INFO -Dspring.config.location=file:/opt/jboss/apache-tomcat-8.0.33/overrides/ -Dspring.profiles.active=dev"
And in the overrides folder I got 2 files
1) application.properties
2) application-dev.properties
The application.properties has a single entry in it
spring.profiles.active=dev
I see that the proper log.level is fed to my code which means this command is working. Its just that I am clueless as to why my override is not happening as expected
I don't have any `PropertyPlaceholderConfigurer code in my workspace. I am not even sure if I need 1
I don't use this method to externalise properties. First, I'll try a suggestion for your method and then I'll show you what I'm using.
The suggestion for your method is to use file:/// instead of file:/ as with Spring I found that when not passing the three slashes after the colon it didn't recognise the property.
I've created a sample project for you, available here with instructions.
Now for the method I use.
I define a Configuration file for each profile and I keep the application.properties file under src/main/resources.
Then I use the #Profile and #PropertySource annotations on each configuration file.
For example:
#Configuration
#Profile("dev")
#PropertySource("file:///${user.home}/.devopsbuddy/application-dev.properties")
public class DevelopmentConfig {
#Bean
public EmailService emailService() {
return new MockEmailService();
}
#Bean
public ServletRegistrationBean h2ConsoleServletRegistration() {
ServletRegistrationBean bean = new ServletRegistrationBean(new WebServlet());
bean.addUrlMappings("/console/*");
return bean;
}
}
And
#Configuration
#Profile("prod")
#PropertySource("file:///${user.home}/.devopsbuddy/application-prod.properties")
public class ProductionConfig {
#Bean
public EmailService emailService() {
return new SmtpEmailService();
}
}
I have also got a Configuration file that is valid for all profiles, which I call ApplicationConfig, as follows:
#Configuration
#EnableJpaRepositories(basePackages = "com.devopsbuddy.backend.persistence.repositories")
#EntityScan(basePackages = "com.devopsbuddy.backend.persistence.domain.backend")
#EnableTransactionManagement
#PropertySource("file:///${user.home}/.devopsbuddy/application-common.properties")
public class ApplicationConfig {
}
My src/main/resources/application.properties file looks like the following:
spring.profiles.active=dev
default.to.address=me#example.com
token.expiration.length.minutes=120
Of course I could externalise the spring.profile.active property by passing it as a system property but for my case and for now it's fine.
When running the application, if I pass the "dev" profile, Spring will load all properties and Beans defined in the DevelopmentConfig class plus all those in ApplicationConfig. If I pass "prod", the ProductionConfig and ApplicationConfig properties will be loaded instead.
I'm completing a course on how to create a Spring Boot website with Security, Email, Data JPA, Amazon Web Services, Stripe and much more. If you want, you can register your interest here and you will get notified when the course is open for enrolment.

Spring boot multi module servletDispatchers

I have a following project structure
-Project
|-config
| |-modules
| |-admin
| |-web
|- platform
Platform is the project that contains the spring-boot start class,
Platform has a dependency on config and config had dependencies on everything in the directory modules
Platform is also the module that gets started with the mvn spring-boot:run command.
The thing I am trying to accomplish is that the modules admin and web (both web apps) have their own mapping like
/admin
/web
The following code represents an controller in the admin module, the web module also contains a similar controller (thats the point)
#Controller
public class AdminController {
#RequestMapping("/")
public String adminController() {
return "admin";
}
}
Here some code for the configuration of the admin module
#Configuration
public class Config implements EmbeddedServletContainerCustomizer {
#Autowired
protected WebApplicationContext webApplicationContext;
#Autowired
protected ServerProperties server;
#Autowired(required = false)
protected MultipartConfigElement multipartConfig;
protected DispatcherServlet createDispatcherServlet() {
AnnotationConfigEmbeddedWebApplicationContext webContext = new AnnotationConfigEmbeddedWebApplicationContext();
webContext.setParent(webApplicationContext);
webContext.scan("some.base.package");
return new DispatcherServlet(webContext);
}
protected ServletRegistrationBean createModuleDispatcher(DispatcherServlet apiModuleDispatcherServlet) {
ServletRegistrationBean registration =
new ServletRegistrationBean(apiModuleDispatcherServlet,
"/admin");
registration.setName("admin");
registration.setMultipartConfig(this.multipartConfig);
return registration;
}
#Bean(name = "adminsServletRegistrationBean")
public ServletRegistrationBean apiModuleADispatcherServletRegistration() {
return createModuleDispatcher(createDispatcherServlet());
}
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setContextPath("/admin");
}
}
Something similar goes for the web module
I have tried the implement the some of the given answers.
Using multiple dispatcher servlets / web contexts with spring boot
Spring Boot (JAR) with multiple dispatcher servlets for different REST APIs with Spring Data REST
And lots of googling
When I let the component scan, scan both modules (removing the ComponentScan filter)
I get an a Ambiguous mapping found exception, saying that both controller methods dispatch to the same path "/"
But when disabling the component scan on one of the modules, then indeed the admin modules get mapped to /admin.
when I disable both controllers, I see that the /web and /admin dispatchServlets get mapped.
So I understand the exception but I dont understand how to resolve this.
For me its a must that I can do this per module and I dont want to map it using
#RequestMapping("/admin")
on the controller class
I also tried specifying the contextPath and servletPath in the application.properties
So my question is: what would be the best approach to reach my goal, or am I trying to use spring-boot for something it was not ment for.
Edit
A Proof of concept would be nice
So I found the solution.
You can take a look here at this link
You`ll have to register the dispatcher servlets in the main application and dont use the #SpringBootApplication annotation.
For a complete example just checkout the project and check the code
EDIT: later this week ill provide a detailed answer
What you should do is that put your AdminController and WebController in separate packages.
some.base.admin.package
some.base.web.package
And in the corresponding configurations scan only your required package to register the DispatcherServlet
Admin Config
webContext.scan("some.base.admin.package");
Web Config
webContext.scan("some.base.web.package");
This way in the corresponding WebApplicationContext for each DispatcherServlet only one mapping will be available for "/"

Categories