001package io.avaje.inject; 002 003import io.avaje.inject.core.BeanContextFactory; 004import io.avaje.inject.core.Builder; 005import io.avaje.inject.core.BuilderFactory; 006import io.avaje.inject.core.EnrichBean; 007import io.avaje.inject.core.SuppliedBean; 008import org.slf4j.Logger; 009import org.slf4j.LoggerFactory; 010 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.HashMap; 014import java.util.Iterator; 015import java.util.LinkedHashSet; 016import java.util.List; 017import java.util.Map; 018import java.util.ServiceLoader; 019import java.util.Set; 020import java.util.function.Consumer; 021 022/** 023 * Build a bean context with options for shutdown hook and supplying test doubles. 024 * <p> 025 * We would choose to use BeanContextBuilder in test code (for component testing) as it gives us 026 * the ability to inject test doubles, mocks, spy's etc. 027 * </p> 028 * 029 * <pre>{@code 030 * 031 * @Test 032 * public void someComponentTest() { 033 * 034 * MyRedisApi mockRedis = mock(MyRedisApi.class); 035 * MyDbApi mockDatabase = mock(MyDbApi.class); 036 * 037 * try (BeanContext context = new BeanContextBuilder() 038 * .withBeans(mockRedis, mockDatabase) 039 * .build()) { 040 * 041 * // built with test doubles injected ... 042 * CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); 043 * coffeeMaker.makeIt(); 044 * 045 * assertThat(... 046 * } 047 * } 048 * 049 * }</pre> 050 */ 051public class BeanContextBuilder { 052 053 private static final Logger log = LoggerFactory.getLogger(BeanContextBuilder.class); 054 055 private boolean shutdownHook = true; 056 057 @SuppressWarnings("rawtypes") 058 private final List<SuppliedBean> suppliedBeans = new ArrayList<>(); 059 060 @SuppressWarnings("rawtypes") 061 private final List<EnrichBean> enrichBeans = new ArrayList<>(); 062 063 private final Set<String> includeModules = new LinkedHashSet<>(); 064 065 private boolean ignoreMissingModuleDependencies; 066 067 /** 068 * Create a BeanContextBuilder to ultimately load and return a new BeanContext. 069 * 070 * <pre>{@code 071 * 072 * try (BeanContext context = new BeanContextBuilder() 073 * .build()) { 074 * 075 * String makeIt = context.getBean(CoffeeMaker.class).makeIt(); 076 * } 077 * }</pre> 078 */ 079 public BeanContextBuilder() { 080 } 081 082 /** 083 * Boot the bean context without registering a shutdown hook. 084 * <p> 085 * The expectation is that the BeanContextBuilder is closed via code or via using 086 * try with resources. 087 * </p> 088 * <pre>{@code 089 * 090 * // automatically closed via try with resources 091 * 092 * try (BeanContext context = new BeanContextBuilder() 093 * .withNoShutdownHook() 094 * .build()) { 095 * 096 * String makeIt = context.getBean(CoffeeMaker.class).makeIt(); 097 * } 098 * 099 * }</pre> 100 * 101 * @return This BeanContextBuilder 102 */ 103 public BeanContextBuilder withNoShutdownHook() { 104 this.shutdownHook = false; 105 return this; 106 } 107 108 /** 109 * Specify the modules to include in dependency injection. 110 * <p/> 111 * This is effectively a "whitelist" of modules names to include in the injection excluding 112 * any other modules that might otherwise exist in the classpath. 113 * <p/> 114 * We typically want to use this in component testing where we wish to exclude any other 115 * modules that exist on the classpath. 116 * 117 * <pre>{@code 118 * 119 * @Test 120 * public void someComponentTest() { 121 * 122 * EmailServiceApi mockEmailService = mock(EmailServiceApi.class); 123 * 124 * try (BeanContext context = new BeanContextBuilder() 125 * .withBeans(mockEmailService) 126 * .withModules("coffee") 127 * .withIgnoreMissingModuleDependencies() 128 * .build()) { 129 * 130 * // built with test doubles injected ... 131 * CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); 132 * coffeeMaker.makeIt(); 133 * 134 * assertThat(... 135 * } 136 * } 137 * 138 * }</pre> 139 * 140 * @param modules The names of modules that we want to include in dependency injection. 141 * @return This BeanContextBuilder 142 */ 143 public BeanContextBuilder withModules(String... modules) { 144 this.includeModules.addAll(Arrays.asList(modules)); 145 return this; 146 } 147 148 /** 149 * Set this when building a BeanContext (typically for testing) and supplied beans replace module dependencies. 150 * This means we don't need the usual module dependencies as supplied beans are used instead. 151 */ 152 public BeanContextBuilder withIgnoreMissingModuleDependencies() { 153 this.ignoreMissingModuleDependencies = true; 154 return this; 155 } 156 157 /** 158 * Supply a bean to the context that will be used instead of any 159 * similar bean in the context. 160 * <p> 161 * This is typically expected to be used in tests and the bean 162 * supplied is typically a test double or mock. 163 * </p> 164 * 165 * <pre>{@code 166 * 167 * Pump pump = mock(Pump.class); 168 * Grinder grinder = mock(Grinder.class); 169 * 170 * try (BeanContext context = new BeanContextBuilder() 171 * .withBeans(pump, grinder) 172 * .build()) { 173 * 174 * CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); 175 * coffeeMaker.makeIt(); 176 * 177 * Pump pump1 = context.getBean(Pump.class); 178 * Grinder grinder1 = context.getBean(Grinder.class); 179 * 180 * assertThat(pump1).isSameAs(pump); 181 * assertThat(grinder1).isSameAs(grinder); 182 * 183 * verify(pump).pumpWater(); 184 * verify(grinder).grindBeans(); 185 * } 186 * 187 * }</pre> 188 * 189 * @param beans The bean used when injecting a dependency for this bean or the interface(s) it implements 190 * @return This BeanContextBuilder 191 */ 192 @SuppressWarnings({"unchecked", "rawtypes"}) 193 public BeanContextBuilder withBeans(Object... beans) { 194 for (Object bean : beans) { 195 suppliedBeans.add(new SuppliedBean(suppliedType(bean.getClass()), bean)); 196 } 197 return this; 198 } 199 200 /** 201 * Add a supplied bean instance with the given injection type. 202 * <p> 203 * This is typically a test double often created by Mockito or similar. 204 * </p> 205 * 206 * <pre>{@code 207 * 208 * try (BeanContext context = new BeanContextBuilder() 209 * .withBean(Pump.class, mock) 210 * .build()) { 211 * 212 * Pump pump = context.getBean(Pump.class); 213 * assertThat(pump).isSameAs(mock); 214 * 215 * // act 216 * CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); 217 * coffeeMaker.makeIt(); 218 * 219 * verify(pump).pumpSteam(); 220 * verify(pump).pumpWater(); 221 * } 222 * 223 * }</pre> 224 * 225 * @param type The dependency injection type this bean is target for 226 * @param bean The supplied bean instance to use (typically a test mock) 227 */ 228 public <D> BeanContextBuilder withBean(Class<D> type, D bean) { 229 suppliedBeans.add(new SuppliedBean<>(type, bean)); 230 return this; 231 } 232 233 /** 234 * Use a mockito mock when injecting this bean type. 235 * 236 * <pre>{@code 237 * 238 * try (BeanContext context = new BeanContextBuilder() 239 * .withMock(Pump.class) 240 * .withMock(Grinder.class, grinder -> { 241 * // setup the mock 242 * when(grinder.grindBeans()).thenReturn("stub response"); 243 * }) 244 * .build()) { 245 * 246 * 247 * CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); 248 * coffeeMaker.makeIt(); 249 * 250 * // this is a mockito mock 251 * Grinder grinder = context.getBean(Grinder.class); 252 * verify(grinder).grindBeans(); 253 * } 254 * 255 * }</pre> 256 */ 257 public BeanContextBuilder withMock(Class<?> type) { 258 return withMock(type, null); 259 } 260 261 /** 262 * Use a mockito mock when injecting this bean type additionally 263 * running setup on the mock instance. 264 * 265 * <pre>{@code 266 * 267 * try (BeanContext context = new BeanContextBuilder() 268 * .withMock(Pump.class) 269 * .withMock(Grinder.class, grinder -> { 270 * 271 * // setup the mock 272 * when(grinder.grindBeans()).thenReturn("stub response"); 273 * }) 274 * .build()) { 275 * 276 * 277 * CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); 278 * coffeeMaker.makeIt(); 279 * 280 * // this is a mockito mock 281 * Grinder grinder = context.getBean(Grinder.class); 282 * verify(grinder).grindBeans(); 283 * } 284 * 285 * }</pre> 286 */ 287 public <D> BeanContextBuilder withMock(Class<D> type, Consumer<D> consumer) { 288 suppliedBeans.add(new SuppliedBean<>(type, null, consumer)); 289 return this; 290 } 291 292 /** 293 * Use a mockito spy when injecting this bean type. 294 * 295 * <pre>{@code 296 * 297 * try (BeanContext context = new BeanContextBuilder() 298 * .withSpy(Pump.class) 299 * .build()) { 300 * 301 * // setup spy here ... 302 * Pump pump = context.getBean(Pump.class); 303 * doNothing().when(pump).pumpSteam(); 304 * 305 * // act 306 * CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); 307 * coffeeMaker.makeIt(); 308 * 309 * verify(pump).pumpWater(); 310 * verify(pump).pumpSteam(); 311 * } 312 * 313 * }</pre> 314 */ 315 public BeanContextBuilder withSpy(Class<?> type) { 316 return withSpy(type, null); 317 } 318 319 /** 320 * Use a mockito spy when injecting this bean type additionally 321 * running setup on the spy instance. 322 * 323 * <pre>{@code 324 * 325 * try (BeanContext context = new BeanContextBuilder() 326 * .withSpy(Pump.class, pump -> { 327 * // setup the spy 328 * doNothing().when(pump).pumpWater(); 329 * }) 330 * .build()) { 331 * 332 * // or setup here ... 333 * Pump pump = context.getBean(Pump.class); 334 * doNothing().when(pump).pumpSteam(); 335 * 336 * // act 337 * CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); 338 * coffeeMaker.makeIt(); 339 * 340 * verify(pump).pumpWater(); 341 * verify(pump).pumpSteam(); 342 * } 343 * 344 * }</pre> 345 */ 346 public <D> BeanContextBuilder withSpy(Class<D> type, Consumer<D> consumer) { 347 enrichBeans.add(new EnrichBean<>(type, consumer)); 348 return this; 349 } 350 351 /** 352 * Build and return the bean context. 353 * 354 * @return The BeanContext 355 */ 356 public BeanContext build() { 357 // sort factories by dependsOn 358 FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty(), ignoreMissingModuleDependencies); 359 ServiceLoader.load(BeanContextFactory.class).forEach(factoryOrder::add); 360 361 Set<String> moduleNames = factoryOrder.orderFactories(); 362 if (moduleNames.isEmpty()) { 363 throw new IllegalStateException("No modules found suggests using Gradle and IDEA but with a setup issue?" + 364 " Review IntelliJ Settings / Build / Build tools / Gradle - 'Build and run using' value and set that to 'Gradle'. " + 365 " Refer to https://dinject.io/docs/gradle#idea"); 366 } 367 log.debug("building context with modules {}", moduleNames); 368 Builder rootBuilder = BuilderFactory.newRootBuilder(suppliedBeans, enrichBeans); 369 for (BeanContextFactory factory : factoryOrder.factories()) { 370 rootBuilder.addChild(factory.createContext(rootBuilder)); 371 } 372 373 BeanContext beanContext = rootBuilder.build(); 374 // entire graph built, fire postConstruct 375 beanContext.start(); 376 if (shutdownHook) { 377 return new ShutdownAwareBeanContext(beanContext); 378 } 379 return beanContext; 380 } 381 382 /** 383 * Return the type that we map the supplied bean to. 384 */ 385 private Class<?> suppliedType(Class<?> suppliedClass) { 386 Class<?> suppliedSuper = suppliedClass.getSuperclass(); 387 if (Object.class.equals(suppliedSuper)) { 388 return suppliedClass; 389 } else { 390 // prefer to use the super type of the supplied bean (test double) 391 return suppliedSuper; 392 } 393 } 394 395 /** 396 * Internal shutdown hook. 397 */ 398 private static class Hook extends Thread { 399 400 private final ShutdownAwareBeanContext context; 401 402 Hook(ShutdownAwareBeanContext context) { 403 this.context = context; 404 } 405 406 @Override 407 public void run() { 408 context.shutdown(); 409 } 410 } 411 412 /** 413 * Proxy that handles shutdown hook registration and de-registration. 414 */ 415 private static class ShutdownAwareBeanContext implements BeanContext { 416 417 private final BeanContext context; 418 private final Hook hook; 419 private boolean shutdown; 420 421 ShutdownAwareBeanContext(BeanContext context) { 422 this.context = context; 423 this.hook = new Hook(this); 424 Runtime.getRuntime().addShutdownHook(hook); 425 } 426 427 @Override 428 public String getName() { 429 return context.getName(); 430 } 431 432 @Override 433 public String[] getProvides() { 434 return context.getProvides(); 435 } 436 437 @Override 438 public String[] getDependsOn() { 439 return context.getDependsOn(); 440 } 441 442 @Override 443 public <T> T getBean(Class<T> beanClass) { 444 return context.getBean(beanClass); 445 } 446 447 @Override 448 public <T> T getBean(Class<T> beanClass, String name) { 449 return context.getBean(beanClass, name); 450 } 451 452 @Override 453 public <T> BeanEntry<T> candidate(Class<T> type, String name) { 454 return context.candidate(type, name); 455 } 456 457 @Override 458 public List<Object> getBeansWithAnnotation(Class<?> annotation) { 459 return context.getBeansWithAnnotation(annotation); 460 } 461 462 @Override 463 public <T> List<T> getBeans(Class<T> interfaceType) { 464 return context.getBeans(interfaceType); 465 } 466 467 @Override 468 public <T> List<T> getBeansByPriority(Class<T> interfaceType) { 469 return context.getBeansByPriority(interfaceType); 470 } 471 472 @Override 473 public <T> List<T> sortByPriority(List<T> list) { 474 return context.sortByPriority(list); 475 } 476 477 @Override 478 public void start() { 479 context.start(); 480 } 481 482 @Override 483 public void close() { 484 synchronized (this) { 485 if (!shutdown) { 486 Runtime.getRuntime().removeShutdownHook(hook); 487 } 488 context.close(); 489 } 490 } 491 492 /** 493 * Close via shutdown hook. 494 */ 495 void shutdown() { 496 synchronized (this) { 497 shutdown = true; 498 close(); 499 } 500 } 501 } 502 503 /** 504 * Helper to order the BeanContextFactory based on dependsOn. 505 */ 506 static class FactoryOrder { 507 508 private final Set<String> includeModules; 509 private final boolean suppliedBeans; 510 private final boolean ignoreMissingModuleDependencies; 511 512 private final Set<String> moduleNames = new LinkedHashSet<>(); 513 private final List<BeanContextFactory> factories = new ArrayList<>(); 514 private final List<FactoryState> queue = new ArrayList<>(); 515 private final List<FactoryState> queueNoDependencies = new ArrayList<>(); 516 517 private final Map<String, FactoryList> providesMap = new HashMap<>(); 518 519 FactoryOrder(Set<String> includeModules, boolean suppliedBeans, boolean ignoreMissingModuleDependencies) { 520 this.includeModules = includeModules; 521 this.suppliedBeans = suppliedBeans; 522 this.ignoreMissingModuleDependencies = ignoreMissingModuleDependencies; 523 } 524 525 void add(BeanContextFactory factory) { 526 527 if (includeModule(factory)) { 528 FactoryState wrappedFactory = new FactoryState(factory); 529 providesMap.computeIfAbsent(factory.getName(), s -> new FactoryList()).add(wrappedFactory); 530 if (!isEmpty(factory.getProvides())) { 531 for (String feature : factory.getProvides()) { 532 providesMap.computeIfAbsent(feature, s -> new FactoryList()).add(wrappedFactory); 533 } 534 } 535 if (isEmpty(factory.getDependsOn())) { 536 if (!isEmpty(factory.getProvides())) { 537 // only has 'provides' so we can push this 538 push(wrappedFactory); 539 } else { 540 // hold until after all the 'provides only' modules are added 541 queueNoDependencies.add(wrappedFactory); 542 } 543 } else { 544 // queue it to process by dependency ordering 545 queue.add(wrappedFactory); 546 } 547 } 548 } 549 550 private boolean isEmpty(String[] values) { 551 return values == null || values.length == 0; 552 } 553 554 /** 555 * Return true of the factory (for the module) should be included. 556 */ 557 private boolean includeModule(BeanContextFactory factory) { 558 return includeModules.isEmpty() || includeModules.contains(factory.getName()); 559 } 560 561 /** 562 * Push the factory onto the build order (the wiring order for modules). 563 */ 564 private void push(FactoryState factory) { 565 factory.setPushed(); 566 factories.add(factory.getFactory()); 567 moduleNames.add(factory.getName()); 568 } 569 570 /** 571 * Order the factories returning the ordered list of module names. 572 */ 573 Set<String> orderFactories() { 574 // push the 'no dependency' modules after the 'provides only' ones 575 // as this is more intuitive for the simple (only provides modules case) 576 for (FactoryState factoryState : queueNoDependencies) { 577 push(factoryState); 578 } 579 processQueue(); 580 return moduleNames; 581 } 582 583 /** 584 * Return the list of factories in the order they should be built. 585 */ 586 List<BeanContextFactory> factories() { 587 return factories; 588 } 589 590 /** 591 * Process the queue pushing the factories in order to satisfy dependencies. 592 */ 593 private void processQueue() { 594 595 int count; 596 do { 597 count = processQueuedFactories(); 598 } while (count > 0); 599 600 if (suppliedBeans || ignoreMissingModuleDependencies) { 601 // just push everything left assuming supplied beans 602 // will satisfy the required dependencies 603 for (FactoryState factoryState : queue) { 604 push(factoryState); 605 } 606 607 } else if (!queue.isEmpty()) { 608 StringBuilder sb = new StringBuilder(); 609 for (FactoryState factory : queue) { 610 sb.append("Module [").append(factory.getName()).append("] has unsatisfied dependencies on modules:"); 611 for (String depModuleName : factory.getDependsOn()) { 612 if (!moduleNames.contains(depModuleName)) { 613 sb.append(String.format(" [%s]", depModuleName)); 614 } 615 } 616 } 617 618 sb.append(". Modules that were loaded ok are:").append(moduleNames); 619 sb.append(". Consider using BeanContextBuilder.withIgnoreMissingModuleDependencies() or BeanContextBuilder.withSuppliedBeans(...)"); 620 throw new IllegalStateException(sb.toString()); 621 } 622 } 623 624 /** 625 * Process the queued factories pushing them when all their (module) dependencies 626 * are satisfied. 627 * <p> 628 * This returns the number of factories added so once this returns 0 it is done. 629 */ 630 private int processQueuedFactories() { 631 632 int count = 0; 633 Iterator<FactoryState> it = queue.iterator(); 634 while (it.hasNext()) { 635 FactoryState factory = it.next(); 636 if (satisfiedDependencies(factory)) { 637 // push the factory onto the build order 638 it.remove(); 639 push(factory); 640 count++; 641 } 642 } 643 return count; 644 } 645 646 /** 647 * Return true if the (module) dependencies are satisfied for this factory. 648 */ 649 private boolean satisfiedDependencies(FactoryState factory) { 650 for (String moduleOrFeature : factory.getDependsOn()) { 651 FactoryList factories = providesMap.get(moduleOrFeature); 652 if (factories == null || !factories.allPushed()) { 653 return false; 654 } 655 } 656 return true; 657 } 658 } 659 660 /** 661 * Wrapper on Factory holding the pushed state. 662 */ 663 private static class FactoryState { 664 665 private final BeanContextFactory factory; 666 private boolean pushed; 667 668 private FactoryState(BeanContextFactory factory) { 669 this.factory = factory; 670 } 671 672 /** 673 * Set when factory is pushed onto the build/wiring order. 674 */ 675 void setPushed() { 676 this.pushed = true; 677 } 678 679 boolean isPushed() { 680 return pushed; 681 } 682 683 BeanContextFactory getFactory() { 684 return factory; 685 } 686 687 String getName() { 688 return factory.getName(); 689 } 690 691 String[] getDependsOn() { 692 return factory.getDependsOn(); 693 } 694 } 695 696 /** 697 * List of factories for a given name or feature. 698 */ 699 private static class FactoryList { 700 701 private final List<FactoryState> factories = new ArrayList<>(); 702 703 void add(FactoryState factory) { 704 factories.add(factory); 705 } 706 707 /** 708 * Return true if all factories here have been pushed onto the build order. 709 */ 710 boolean allPushed() { 711 for (FactoryState factory : factories) { 712 if (!factory.isPushed()) { 713 return false; 714 } 715 } 716 return true; 717 } 718 } 719 720}