001package io.avaje.inject; 002 003import io.avaje.inject.spi.Module; 004 005import java.lang.reflect.Type; 006import java.util.function.Consumer; 007 008/** 009 * Build a bean scope with options for shutdown hook and supplying external dependencies. 010 * <p> 011 * We can provide external dependencies that are then used in wiring the components. 012 * </p> 013 * 014 * <pre>{@code 015 * 016 * // external dependencies 017 * Pump pump = ... 018 * 019 * BeanScope scope = BeanScope.newBuilder() 020 * .withBean(pump) 021 * .build(); 022 * 023 * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class); 024 * coffeeMaker.makeIt(); 025 * 026 * }</pre> 027 */ 028public interface BeanScopeBuilder { 029 030 /** 031 * Create the bean scope registering a shutdown hook (defaults to false, no shutdown hook). 032 * <p> 033 * With {@code withShutdownHook(true)} a shutdown hook will be registered with the Runtime 034 * and executed when the JVM initiates a shutdown. This then will run the {@code preDestroy} 035 * lifecycle methods. 036 * </p> 037 * <pre>{@code 038 * 039 * // automatically closed via try with resources 040 * 041 * BeanScope scope = BeanScope.newBuilder() 042 * .withShutdownHook(true) 043 * .build()); 044 * 045 * // on JVM shutdown the preDestroy lifecycle methods are executed 046 * 047 * }</pre> 048 * 049 * @return This BeanScopeBuilder 050 */ 051 BeanScopeBuilder withShutdownHook(boolean shutdownHook); 052 053 /** 054 * Specify the modules to include in dependency injection. 055 * <p> 056 * Only beans related to the module are included in the BeanScope that is built. 057 * <p> 058 * When we do not explicitly specify modules then all modules that are not "custom scoped" 059 * are found and used via service loading. 060 * 061 * <pre>{@code 062 * 063 * BeanScope scope = BeanScope.newBuilder() 064 * .withModules(new CustomModule()) 065 * .build()); 066 * 067 * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class); 068 * coffeeMaker.makeIt(); 069 * 070 * }</pre> 071 * 072 * @param modules The modules that we want to include in dependency injection. 073 * @return This BeanScopeBuilder 074 */ 075 BeanScopeBuilder withModules(Module... modules); 076 077 /** 078 * Supply a bean to the scope that will be used instead of any 079 * similar bean in the scope. 080 * <p> 081 * This is typically expected to be used in tests and the bean 082 * supplied is typically a test double or mock. 083 * </p> 084 * 085 * <pre>{@code 086 * 087 * // external dependencies 088 * Pump pump = ... 089 * Grinder grinder = ... 090 * 091 * BeanScope scope = BeanScope.newBuilder() 092 * .withBeans(pump, grinder) 093 * .build(); 094 * 095 * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class); 096 * coffeeMaker.makeIt(); 097 * 098 * }</pre> 099 * 100 * @param beans Externally provided beans used when injecting a dependency 101 * for the bean or the interface(s) it implements 102 * @return This BeanScopeBuilder 103 */ 104 BeanScopeBuilder withBeans(Object... beans); 105 106 /** 107 * Add a supplied bean instance with the given injection type (typically an interface type). 108 * 109 * <pre>{@code 110 * 111 * Pump externalDependency = ... 112 * 113 * try (BeanScope scope = BeanScope.newBuilder() 114 * .withBean(Pump.class, externalDependency) 115 * .build()) { 116 * 117 * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class); 118 * coffeeMaker.makeIt(); 119 * 120 * Pump pump = scope.get(Pump.class); 121 * assertThat(pump).isSameAs(externalDependency); 122 * } 123 * 124 * }</pre> 125 * 126 * @param type The dependency injection type this bean is target for 127 * @param bean The supplied bean instance to use for injection 128 */ 129 <D> BeanScopeBuilder withBean(Class<D> type, D bean); 130 131 /** 132 * Add a supplied bean instance with the given name and injection type. 133 * 134 * @param name The name qualifier 135 * @param type The dependency injection type this bean is target for 136 * @param bean The supplied bean instance to use for injection 137 */ 138 <D> BeanScopeBuilder withBean(String name, Class<D> type, D bean); 139 140 /** 141 * Add a supplied bean instance with the given name and generic type. 142 * 143 * @param name The name qualifier 144 * @param type The dependency injection type this bean is target for 145 * @param bean The supplied bean instance to use for injection 146 */ 147 <D> BeanScopeBuilder withBean(String name, Type type, D bean); 148 149 /** 150 * Add a supplied bean instance with a generic type. 151 * 152 * @param type The dependency injection type this bean is target for 153 * @param bean The supplied bean instance to use for injection 154 */ 155 <D> BeanScopeBuilder withBean(Type type, D bean); 156 157 /** 158 * Use the given BeanScope as the parent. This becomes an additional 159 * source of beans that can be wired and accessed in this scope. 160 * 161 * @param parent The BeanScope that acts as the parent 162 */ 163 BeanScopeBuilder withParent(BeanScope parent); 164 165 /** 166 * Extend the builder to support testing using mockito with 167 * <code>withMock()</code> and <code>withSpy()</code> methods. 168 * 169 * @return The builder with extra testing support for mockito mocks and spies 170 */ 171 BeanScopeBuilder.ForTesting forTesting(); 172 173 /** 174 * Build and return the bean scope. 175 * <p> 176 * The BeanScope is effectively immutable in that all components are created 177 * and all PostConstruct lifecycle methods have been invoked. 178 * <p> 179 * The beanScope effectively contains eager singletons. 180 * 181 * @return The BeanScope 182 */ 183 BeanScope build(); 184 185 /** 186 * Extends the building with testing specific support for mocks and spies. 187 */ 188 interface ForTesting extends BeanScopeBuilder { 189 190 /** 191 * Use a mockito mock when injecting this bean type. 192 * 193 * <pre>{@code 194 * 195 * try (BeanScope scope = BeanScope.newBuilder() 196 * .forTesting() 197 * .withMock(Pump.class) 198 * .withMock(Grinder.class) 199 * .build()) { 200 * 201 * 202 * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class); 203 * coffeeMaker.makeIt(); 204 * 205 * // this is a mockito mock 206 * Grinder grinder = scope.get(Grinder.class); 207 * verify(grinder).grindBeans(); 208 * } 209 * 210 * }</pre> 211 */ 212 BeanScopeBuilder.ForTesting withMock(Class<?> type); 213 214 /** 215 * Register as a Mockito mock with a qualifier name. 216 * 217 * <pre>{@code 218 * 219 * try (BeanScope scope = BeanScope.newBuilder() 220 * .forTesting() 221 * .withMock(Store.class, "red") 222 * .withMock(Store.class, "blue") 223 * .build()) { 224 * 225 * ... 226 * } 227 * 228 * }</pre> 229 */ 230 BeanScopeBuilder.ForTesting withMock(Class<?> type, String name); 231 232 /** 233 * Use a mockito mock when injecting this bean type additionally 234 * running setup on the mock instance. 235 * 236 * <pre>{@code 237 * 238 * try (BeanScope scope = BeanScope.newBuilder() 239 * .forTesting() 240 * .withMock(Pump.class) 241 * .withMock(Grinder.class, grinder -> { 242 * 243 * // setup the mock 244 * when(grinder.grindBeans()).thenReturn("stub response"); 245 * }) 246 * .build()) { 247 * 248 * 249 * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class); 250 * coffeeMaker.makeIt(); 251 * 252 * // this is a mockito mock 253 * Grinder grinder = scope.get(Grinder.class); 254 * verify(grinder).grindBeans(); 255 * } 256 * 257 * }</pre> 258 */ 259 <D> BeanScopeBuilder.ForTesting withMock(Class<D> type, Consumer<D> consumer); 260 261 /** 262 * Use a mockito spy when injecting this bean type. 263 * 264 * <pre>{@code 265 * 266 * try (BeanScope scope = BeanScope.newBuilder() 267 * .forTesting() 268 * .withSpy(Pump.class) 269 * .build()) { 270 * 271 * // setup spy here ... 272 * Pump pump = scope.get(Pump.class); 273 * doNothing().when(pump).pumpSteam(); 274 * 275 * // act 276 * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class); 277 * coffeeMaker.makeIt(); 278 * 279 * verify(pump).pumpWater(); 280 * verify(pump).pumpSteam(); 281 * } 282 * 283 * }</pre> 284 */ 285 BeanScopeBuilder.ForTesting withSpy(Class<?> type); 286 287 /** 288 * Register a Mockito spy with a qualifier name. 289 * 290 * <pre>{@code 291 * 292 * try (BeanScope scope = BeanScope.newBuilder() 293 * .forTesting() 294 * .withSpy(Store.class, "red") 295 * .withSpy(Store.class, "blue") 296 * .build()) { 297 * 298 * ... 299 * } 300 * 301 * }</pre> 302 */ 303 BeanScopeBuilder.ForTesting withSpy(Class<?> type, String name); 304 305 /** 306 * Use a mockito spy when injecting this bean type additionally 307 * running setup on the spy instance. 308 * 309 * <pre>{@code 310 * 311 * try (BeanScope scope = BeanScope.newBuilder() 312 * .forTesting() 313 * .withSpy(Pump.class, pump -> { 314 * // setup the spy 315 * doNothing().when(pump).pumpWater(); 316 * }) 317 * .build()) { 318 * 319 * // or setup here ... 320 * Pump pump = scope.get(Pump.class); 321 * doNothing().when(pump).pumpSteam(); 322 * 323 * // act 324 * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class); 325 * coffeeMaker.makeIt(); 326 * 327 * verify(pump).pumpWater(); 328 * verify(pump).pumpSteam(); 329 * } 330 * 331 * }</pre> 332 */ 333 <D> BeanScopeBuilder.ForTesting withSpy(Class<D> type, Consumer<D> consumer); 334 335 } 336}