001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.builder; 018 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.LinkedHashMap; 024import java.util.LinkedHashSet; 025import java.util.Map; 026import java.util.Properties; 027import java.util.Set; 028import java.util.concurrent.atomic.AtomicBoolean; 029 030import org.springframework.beans.factory.support.BeanNameGenerator; 031import org.springframework.boot.Banner; 032import org.springframework.boot.SpringApplication; 033import org.springframework.boot.WebApplicationType; 034import org.springframework.boot.convert.ApplicationConversionService; 035import org.springframework.context.ApplicationContext; 036import org.springframework.context.ApplicationContextInitializer; 037import org.springframework.context.ApplicationListener; 038import org.springframework.context.ConfigurableApplicationContext; 039import org.springframework.core.env.ConfigurableEnvironment; 040import org.springframework.core.env.Environment; 041import org.springframework.core.io.ResourceLoader; 042import org.springframework.util.StringUtils; 043 044/** 045 * Builder for {@link SpringApplication} and {@link ApplicationContext} instances with 046 * convenient fluent API and context hierarchy support. Simple example of a context 047 * hierarchy: 048 * 049 * <pre class="code"> 050 * new SpringApplicationBuilder(ParentConfig.class).child(ChildConfig.class).run(args); 051 * </pre> 052 * 053 * Another common use case is setting active profiles and default properties to set up the 054 * environment for an application: 055 * 056 * <pre class="code"> 057 * new SpringApplicationBuilder(Application.class).profiles("server") 058 * .properties("transport=local").run(args); 059 * </pre> 060 * 061 * <p> 062 * If your needs are simpler, consider using the static convenience methods in 063 * SpringApplication instead. 064 * 065 * @author Dave Syer 066 * @author Andy Wilkinson 067 * @see SpringApplication 068 */ 069public class SpringApplicationBuilder { 070 071 private final SpringApplication application; 072 073 private ConfigurableApplicationContext context; 074 075 private SpringApplicationBuilder parent; 076 077 private final AtomicBoolean running = new AtomicBoolean(false); 078 079 private final Set<Class<?>> sources = new LinkedHashSet<>(); 080 081 private final Map<String, Object> defaultProperties = new LinkedHashMap<>(); 082 083 private ConfigurableEnvironment environment; 084 085 private Set<String> additionalProfiles = new LinkedHashSet<>(); 086 087 private boolean registerShutdownHookApplied; 088 089 private boolean configuredAsChild = false; 090 091 public SpringApplicationBuilder(Class<?>... sources) { 092 this.application = createSpringApplication(sources); 093 } 094 095 /** 096 * Creates a new {@link org.springframework.boot.SpringApplication} instances from the 097 * given sources. Subclasses may override in order to provide a custom subclass of 098 * {@link org.springframework.boot.SpringApplication} 099 * @param sources the sources 100 * @return the {@link org.springframework.boot.SpringApplication} instance 101 * @since 1.1.0 102 */ 103 protected SpringApplication createSpringApplication(Class<?>... sources) { 104 return new SpringApplication(sources); 105 } 106 107 /** 108 * Accessor for the current application context. 109 * @return the current application context (or null if not yet running) 110 */ 111 public ConfigurableApplicationContext context() { 112 return this.context; 113 } 114 115 /** 116 * Accessor for the current application. 117 * @return the current application (never null) 118 */ 119 public SpringApplication application() { 120 return this.application; 121 } 122 123 /** 124 * Create an application context (and its parent if specified) with the command line 125 * args provided. The parent is run first with the same arguments if has not yet been 126 * started. 127 * @param args the command line arguments 128 * @return an application context created from the current state 129 */ 130 public ConfigurableApplicationContext run(String... args) { 131 if (this.running.get()) { 132 // If already created we just return the existing context 133 return this.context; 134 } 135 configureAsChildIfNecessary(args); 136 if (this.running.compareAndSet(false, true)) { 137 synchronized (this.running) { 138 // If not already running copy the sources over and then run. 139 this.context = build().run(args); 140 } 141 } 142 return this.context; 143 } 144 145 private void configureAsChildIfNecessary(String... args) { 146 if (this.parent != null && !this.configuredAsChild) { 147 this.configuredAsChild = true; 148 if (!this.registerShutdownHookApplied) { 149 this.application.setRegisterShutdownHook(false); 150 } 151 initializers(new ParentContextApplicationContextInitializer( 152 this.parent.run(args))); 153 } 154 } 155 156 /** 157 * Returns a fully configured {@link SpringApplication} that is ready to run. 158 * @return the fully configured {@link SpringApplication}. 159 */ 160 public SpringApplication build() { 161 return build(new String[0]); 162 } 163 164 /** 165 * Returns a fully configured {@link SpringApplication} that is ready to run. Any 166 * parent that has been configured will be run with the given {@code args}. 167 * @param args the parent's args 168 * @return the fully configured {@link SpringApplication}. 169 */ 170 public SpringApplication build(String... args) { 171 configureAsChildIfNecessary(args); 172 this.application.addPrimarySources(this.sources); 173 return this.application; 174 } 175 176 /** 177 * Create a child application with the provided sources. Default args and environment 178 * are copied down into the child, but everything else is a clean sheet. 179 * @param sources the sources for the application (Spring configuration) 180 * @return the child application builder 181 */ 182 public SpringApplicationBuilder child(Class<?>... sources) { 183 SpringApplicationBuilder child = new SpringApplicationBuilder(); 184 child.sources(sources); 185 186 // Copy environment stuff from parent to child 187 child.properties(this.defaultProperties).environment(this.environment) 188 .additionalProfiles(this.additionalProfiles); 189 child.parent = this; 190 191 // It's not possible if embedded web server are enabled to support web contexts as 192 // parents because the servlets cannot be initialized at the right point in 193 // lifecycle. 194 web(WebApplicationType.NONE); 195 196 // Probably not interested in multiple banners 197 bannerMode(Banner.Mode.OFF); 198 199 // Make sure sources get copied over 200 this.application.addPrimarySources(this.sources); 201 202 return child; 203 } 204 205 /** 206 * Add a parent application with the provided sources. Default args and environment 207 * are copied up into the parent, but everything else is a clean sheet. 208 * @param sources the sources for the application (Spring configuration) 209 * @return the parent builder 210 */ 211 public SpringApplicationBuilder parent(Class<?>... sources) { 212 if (this.parent == null) { 213 this.parent = new SpringApplicationBuilder(sources) 214 .web(WebApplicationType.NONE).properties(this.defaultProperties) 215 .environment(this.environment); 216 } 217 else { 218 this.parent.sources(sources); 219 } 220 return this.parent; 221 } 222 223 private SpringApplicationBuilder runAndExtractParent(String... args) { 224 if (this.context == null) { 225 run(args); 226 } 227 if (this.parent != null) { 228 return this.parent; 229 } 230 throw new IllegalStateException( 231 "No parent defined yet (please use the other overloaded parent methods to set one)"); 232 } 233 234 /** 235 * Add an already running parent context to an existing application. 236 * @param parent the parent context 237 * @return the current builder (not the parent) 238 */ 239 public SpringApplicationBuilder parent(ConfigurableApplicationContext parent) { 240 this.parent = new SpringApplicationBuilder(); 241 this.parent.context = parent; 242 this.parent.running.set(true); 243 return this; 244 } 245 246 /** 247 * Create a sibling application (one with the same parent). A side effect of calling 248 * this method is that the current application (and its parent) are started without 249 * any arguments if they are not already running. To supply arguments when starting 250 * the current application and its parent use {@link #sibling(Class[], String...)} 251 * instead. 252 * @param sources the sources for the application (Spring configuration) 253 * @return the new sibling builder 254 */ 255 public SpringApplicationBuilder sibling(Class<?>... sources) { 256 return runAndExtractParent().child(sources); 257 } 258 259 /** 260 * Create a sibling application (one with the same parent). A side effect of calling 261 * this method is that the current application (and its parent) are started if they 262 * are not already running. 263 * @param sources the sources for the application (Spring configuration) 264 * @param args the command line arguments to use when starting the current app and its 265 * parent 266 * @return the new sibling builder 267 */ 268 public SpringApplicationBuilder sibling(Class<?>[] sources, String... args) { 269 return runAndExtractParent(args).child(sources); 270 } 271 272 /** 273 * Explicitly set the context class to be used. 274 * @param cls the context class to use 275 * @return the current builder 276 */ 277 public SpringApplicationBuilder contextClass( 278 Class<? extends ConfigurableApplicationContext> cls) { 279 this.application.setApplicationContextClass(cls); 280 return this; 281 } 282 283 /** 284 * Add more sources (configuration classes and components) to this application. 285 * @param sources the sources to add 286 * @return the current builder 287 */ 288 public SpringApplicationBuilder sources(Class<?>... sources) { 289 this.sources.addAll(new LinkedHashSet<>(Arrays.asList(sources))); 290 return this; 291 } 292 293 /** 294 * Flag to explicitly request a specific type of web application. Auto-detected based 295 * on the classpath if not set. 296 * @param webApplicationType the type of web application 297 * @return the current builder 298 * @since 2.0.0 299 */ 300 public SpringApplicationBuilder web(WebApplicationType webApplicationType) { 301 this.application.setWebApplicationType(webApplicationType); 302 return this; 303 } 304 305 /** 306 * Flag to indicate the startup information should be logged. 307 * @param logStartupInfo the flag to set. Default true. 308 * @return the current builder 309 */ 310 public SpringApplicationBuilder logStartupInfo(boolean logStartupInfo) { 311 this.application.setLogStartupInfo(logStartupInfo); 312 return this; 313 } 314 315 /** 316 * Sets the {@link Banner} instance which will be used to print the banner when no 317 * static banner file is provided. 318 * @param banner the banner to use 319 * @return the current builder 320 */ 321 public SpringApplicationBuilder banner(Banner banner) { 322 this.application.setBanner(banner); 323 return this; 324 } 325 326 public SpringApplicationBuilder bannerMode(Banner.Mode bannerMode) { 327 this.application.setBannerMode(bannerMode); 328 return this; 329 } 330 331 /** 332 * Sets if the application is headless and should not instantiate AWT. Defaults to 333 * {@code true} to prevent java icons appearing. 334 * @param headless if the application is headless 335 * @return the current builder 336 */ 337 public SpringApplicationBuilder headless(boolean headless) { 338 this.application.setHeadless(headless); 339 return this; 340 } 341 342 /** 343 * Sets if the created {@link ApplicationContext} should have a shutdown hook 344 * registered. 345 * @param registerShutdownHook if the shutdown hook should be registered 346 * @return the current builder 347 */ 348 public SpringApplicationBuilder registerShutdownHook(boolean registerShutdownHook) { 349 this.registerShutdownHookApplied = true; 350 this.application.setRegisterShutdownHook(registerShutdownHook); 351 return this; 352 } 353 354 /** 355 * Fixes the main application class that is used to anchor the startup messages. 356 * @param mainApplicationClass the class to use. 357 * @return the current builder 358 */ 359 public SpringApplicationBuilder main(Class<?> mainApplicationClass) { 360 this.application.setMainApplicationClass(mainApplicationClass); 361 return this; 362 } 363 364 /** 365 * Flag to indicate that command line arguments should be added to the environment. 366 * @param addCommandLineProperties the flag to set. Default true. 367 * @return the current builder 368 */ 369 public SpringApplicationBuilder addCommandLineProperties( 370 boolean addCommandLineProperties) { 371 this.application.setAddCommandLineProperties(addCommandLineProperties); 372 return this; 373 } 374 375 /** 376 * Flag to indicate if the {@link ApplicationConversionService} should be added to the 377 * application context's {@link Environment}. 378 * @param addConversionService if the conversion service should be added. 379 * @return the current builder 380 * @since 2.1.0 381 */ 382 public SpringApplicationBuilder setAddConversionService( 383 boolean addConversionService) { 384 this.application.setAddConversionService(addConversionService); 385 return this; 386 } 387 388 /** 389 * Default properties for the environment in the form {@code key=value} or 390 * {@code key:value}. 391 * @param defaultProperties the properties to set. 392 * @return the current builder 393 */ 394 public SpringApplicationBuilder properties(String... defaultProperties) { 395 return properties(getMapFromKeyValuePairs(defaultProperties)); 396 } 397 398 private Map<String, Object> getMapFromKeyValuePairs(String[] properties) { 399 Map<String, Object> map = new HashMap<>(); 400 for (String property : properties) { 401 int index = lowestIndexOf(property, ":", "="); 402 String key = (index > 0) ? property.substring(0, index) : property; 403 String value = (index > 0) ? property.substring(index + 1) : ""; 404 map.put(key, value); 405 } 406 return map; 407 } 408 409 private int lowestIndexOf(String property, String... candidates) { 410 int index = -1; 411 for (String candidate : candidates) { 412 int candidateIndex = property.indexOf(candidate); 413 if (candidateIndex > 0) { 414 index = (index != -1) ? Math.min(index, candidateIndex) : candidateIndex; 415 } 416 } 417 return index; 418 } 419 420 /** 421 * Default properties for the environment in the form {@code key=value} or 422 * {@code key:value}. 423 * @param defaultProperties the properties to set. 424 * @return the current builder 425 */ 426 public SpringApplicationBuilder properties(Properties defaultProperties) { 427 return properties(getMapFromProperties(defaultProperties)); 428 } 429 430 private Map<String, Object> getMapFromProperties(Properties properties) { 431 Map<String, Object> map = new HashMap<>(); 432 for (Object key : Collections.list(properties.propertyNames())) { 433 map.put((String) key, properties.get(key)); 434 } 435 return map; 436 } 437 438 /** 439 * Default properties for the environment. Multiple calls to this method are 440 * cumulative. 441 * @param defaults the default properties 442 * @return the current builder 443 * @see SpringApplicationBuilder#properties(String...) 444 */ 445 public SpringApplicationBuilder properties(Map<String, Object> defaults) { 446 this.defaultProperties.putAll(defaults); 447 this.application.setDefaultProperties(this.defaultProperties); 448 if (this.parent != null) { 449 this.parent.properties(this.defaultProperties); 450 this.parent.environment(this.environment); 451 } 452 return this; 453 } 454 455 /** 456 * Add to the active Spring profiles for this app (and its parent and children). 457 * @param profiles the profiles to add. 458 * @return the current builder 459 */ 460 public SpringApplicationBuilder profiles(String... profiles) { 461 this.additionalProfiles.addAll(Arrays.asList(profiles)); 462 this.application.setAdditionalProfiles( 463 StringUtils.toStringArray(this.additionalProfiles)); 464 return this; 465 } 466 467 private SpringApplicationBuilder additionalProfiles( 468 Collection<String> additionalProfiles) { 469 this.additionalProfiles = new LinkedHashSet<>(additionalProfiles); 470 this.application.setAdditionalProfiles( 471 StringUtils.toStringArray(this.additionalProfiles)); 472 return this; 473 } 474 475 /** 476 * Bean name generator for automatically generated bean names in the application 477 * context. 478 * @param beanNameGenerator the generator to set. 479 * @return the current builder 480 */ 481 public SpringApplicationBuilder beanNameGenerator( 482 BeanNameGenerator beanNameGenerator) { 483 this.application.setBeanNameGenerator(beanNameGenerator); 484 return this; 485 } 486 487 /** 488 * Environment for the application context. 489 * @param environment the environment to set. 490 * @return the current builder 491 */ 492 public SpringApplicationBuilder environment(ConfigurableEnvironment environment) { 493 this.application.setEnvironment(environment); 494 this.environment = environment; 495 return this; 496 } 497 498 /** 499 * {@link ResourceLoader} for the application context. If a custom class loader is 500 * needed, this is where it would be added. 501 * @param resourceLoader the resource loader to set. 502 * @return the current builder 503 */ 504 public SpringApplicationBuilder resourceLoader(ResourceLoader resourceLoader) { 505 this.application.setResourceLoader(resourceLoader); 506 return this; 507 } 508 509 /** 510 * Add some initializers to the application (applied to the {@link ApplicationContext} 511 * before any bean definitions are loaded). 512 * @param initializers some initializers to add 513 * @return the current builder 514 */ 515 public SpringApplicationBuilder initializers( 516 ApplicationContextInitializer<?>... initializers) { 517 this.application.addInitializers(initializers); 518 return this; 519 } 520 521 /** 522 * Add some listeners to the application (listening for SpringApplication events as 523 * well as regular Spring events once the context is running). Any listeners that are 524 * also {@link ApplicationContextInitializer} will be added to the 525 * {@link #initializers(ApplicationContextInitializer...) initializers} automatically. 526 * @param listeners some listeners to add 527 * @return the current builder 528 */ 529 public SpringApplicationBuilder listeners(ApplicationListener<?>... listeners) { 530 this.application.addListeners(listeners); 531 return this; 532 } 533 534}