3333import java .lang .reflect .InvocationTargetException ;
3434import java .util .ArrayList ;
3535import java .util .Collection ;
36+ import java .util .Collections ;
3637import java .util .IdentityHashMap ;
3738import java .util .LinkedHashSet ;
3839import java .util .List ;
@@ -134,6 +135,7 @@ public Binding<T> to(final T value) {
134135 }
135136
136137 @ Override
138+ @ SuppressWarnings ("unchecked" )
137139 public Binding <T > to (final Class <? extends T > concreteClass ) {
138140 Objects .requireNonNull (concreteClass , "The Binding Value Class must not be null" );
139141
@@ -145,6 +147,16 @@ public Binding<T> to(final Class<? extends T> concreteClass) {
145147 .orElseThrow (() -> new IllegalArgumentException (
146148 "Could not resolve a TypeDescriptor for " + concreteClass ));
147149
150+ // check for a registered custom scope annotation on the concrete class
151+ final var scopeEntry = this .injectionFramework .findScopeEntry (typeDescriptor ).orElse (null );
152+ if (scopeEntry != null ) {
153+ final ValueBinding <T > factory = new SupplierBinding <>(
154+ dependency , () -> (T ) InjectionContext .this .createUnscoped (concreteClass ));
155+ final Binding <?> scoped = scopeEntry .getValue ().scope (factory );
156+ return addBinding (dependency ,
157+ new CustomScopedClassBinding <>(dependency , concreteClass , scopeEntry .getKey (), scoped ));
158+ }
159+
148160 return addBinding (dependency , this .injectionFramework .isSingleton (typeDescriptor )
149161 ? new LazySingletonClassBinding <>(dependency , concreteClass )
150162 : new NonSingletonClassBinding <T >(dependency , concreteClass ));
@@ -462,13 +474,17 @@ public Context validate() {
462474 + " required by [" + edge .from () + "]" );
463475 }
464476
465- // Scope violation: @Singleton depends on NonSingletonClassBinding (prototype)
477+ // Scope violation: @Singleton depends on a narrower-scoped binding
466478 final var fromBinding = this .bindingsByDependency .get (edge .from ());
467479 final var toBinding = this .bindingsByDependency .get (dep );
468- if (fromBinding instanceof LazySingletonClassBinding
469- && toBinding instanceof NonSingletonClassBinding ) {
470- problems .add ("Scope violation: singleton [" + edge .from ()
471- + "] depends on prototype-scoped [" + dep + "]" );
480+ if (fromBinding instanceof LazySingletonClassBinding ) {
481+ if (toBinding instanceof NonSingletonClassBinding ) {
482+ problems .add ("Scope violation: singleton [" + edge .from ()
483+ + "] depends on prototype-scoped [" + dep + "]" );
484+ } else if (toBinding instanceof CustomScopedClassBinding <?> csb ) {
485+ problems .add ("Scope violation: singleton [" + edge .from ()
486+ + "] depends on @" + csb .scopeAnnotation ().getSimpleName () + "-scoped [" + dep + "]" );
487+ }
472488 }
473489 });
474490
@@ -540,6 +556,90 @@ public Context initializeEagerSingletons() {
540556 return this ;
541557 }
542558
559+ @ Override
560+ @ SuppressWarnings ("unchecked" )
561+ public void close () {
562+ // Collect all instantiated singleton bindings
563+ final var instantiatedSingletons = this .bindingsByDependency .values ().stream ()
564+ .filter (LazySingletonClassBinding .class ::isInstance )
565+ .map (b -> (LazySingletonClassBinding <Object >) b )
566+ .filter (b -> b .value ().optional ().isPresent ())
567+ .collect (Collectors .toList ());
568+
569+ if (instantiatedSingletons .isEmpty ()) {
570+ return ;
571+ }
572+
573+ // Build a dependency graph over instantiated singletons only
574+ final var singletonDeps = instantiatedSingletons .stream ()
575+ .map (Binding ::dependency )
576+ .collect (Collectors .toSet ());
577+
578+ final var graphBuilder = Graph .<Dependency >directed ();
579+ instantiatedSingletons .forEach (b -> {
580+ final var bindingDep = b .dependency ();
581+ graphBuilder .addVertex (bindingDep );
582+ this .injectionFramework .getInjectableDescriptor (b .concreteClass ())
583+ .injectionPoints ()
584+ .flatMap (InjectionPoint ::dependencies )
585+ .filter (singletonDeps ::contains )
586+ .forEach (dep -> graphBuilder .addEdge (bindingDep , dep ));
587+ });
588+
589+ final var graph = graphBuilder .build ();
590+
591+ // topologicalSort returns [leaf-first, root-last]; reverse for destruction (dependents first)
592+ final var destroyOrder = new ArrayList <>(Graphs .topologicalSort (graph ));
593+ Collections .reverse (destroyOrder );
594+
595+ // Index bindings by dependency for lookup during destruction
596+ final var depToBinding = instantiatedSingletons .stream ()
597+ .collect (Collectors .toMap (Binding ::dependency , b -> b ));
598+
599+ // Invoke @PreDestroy methods on each singleton in destruction order
600+ destroyOrder .forEach (dep -> {
601+ final var binding = depToBinding .get (dep );
602+ if (binding == null ) {
603+ return ;
604+ }
605+ final var instance = binding .value ().optional ().orElse (null );
606+ if (instance == null ) {
607+ return ;
608+ }
609+ this .injectionFramework .getInjectableDescriptor (binding .concreteClass ())
610+ .preDestroyMethods ()
611+ .map (md -> md .getTrait (MethodType .class ).orElse (null ))
612+ .filter (Objects ::nonNull )
613+ .map (MethodType ::method )
614+ .forEach (method -> {
615+ try {
616+ method .setAccessible (true );
617+ method .invoke (instance );
618+ } catch (final IllegalAccessException | InvocationTargetException e ) {
619+ throw new InjectionException (
620+ "Invoking @PreDestroy method " + method + " on " + instance .getClass (), e );
621+ }
622+ });
623+ });
624+ }
625+
626+ /**
627+ * Creates an instance of the specified class bypassing any registered {@link Binding}s by
628+ * directly instantiating via {@link ResolvableClass}. Used by scope implementations to produce
629+ * a fresh unscoped instance without recursing into the scoped binding.
630+ *
631+ * @param <T> the type
632+ * @param concreteClass the class to instantiate
633+ * @return a new injected instance
634+ */
635+ @ SuppressWarnings ("unchecked" )
636+ private <T > T createUnscoped (final Class <? extends T > concreteClass ) {
637+ final var codeModel = this .injectionFramework .codeModel ();
638+ final var typeUsage = codeModel .getTypeUsage (concreteClass );
639+ final var dependency = IndependentDependency .of (typeUsage , this .injectionFramework ::getQualifierAnnotationTypes );
640+ return (T ) new ResolvableClass <>(Optional .empty (), dependency , concreteClass ).resolve ();
641+ }
642+
543643 @ Override
544644 public Context newContext () {
545645 return this .injectionFramework .newContext (this .resolver ());
0 commit comments