Last active
March 18, 2026 04:54
-
-
Save DattatreyaReddy/999dde7808f27889f3e4641e0939a5ab to your computer and use it in GitHub Desktop.
Client Service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// Old way | |
| @Component | |
| public class PaymentServiceFactory { | |
| @Autowired private FkPaymentService fkService; | |
| @Autowired private CtPaymentService ctService; | |
| public PaymentService getService(String client) { | |
| switch (client) { | |
| case "FK": return fkService; | |
| case "CT": return ctService; | |
| default: throw new IllegalArgumentException(); | |
| } | |
| } | |
| } | |
| /// New Way | |
| // Simple case: single client, service type inferred from interface | |
| @Client("clientA") | |
| @Service | |
| public class ClientAPaymentService implements PaymentService { ... } | |
| // Explicit service type specification | |
| @Client(value = { "clientB" }, type = PaymentService.class) | |
| @Service | |
| public class ClientBPaymentService implements PaymentService { ... } | |
| // Multiple clients sharing the same implementation | |
| @Client( { "clientC", "clientD" } ) | |
| @Service | |
| public class SharedPaymentService implements PaymentService { ... } | |
| // Usage: | |
| @Autowired | |
| private ClientRegistry clientRegistry | |
| PaymentService service = clientRegistry.getBean("FK", PaymentService.class); | |
| service.process() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import java.lang.annotation.*; | |
| /** | |
| * Annotation used to mark beans that are client-specific implementations. | |
| * Beans annotated with this annotation are automatically discovered and | |
| * registered | |
| * by the {@link ClientRegistry} for client-based bean lookup. | |
| * <p> | |
| * This annotation enables a multi-tenant bean architecture where multiple | |
| * implementations of the same bean interface can coexist, each mapped to one | |
| * or more client identifiers. The registry uses the client identifier to | |
| * retrieve | |
| * the appropriate bean implementation at runtime. | |
| * <p> | |
| * <strong>Type Resolution:</strong> | |
| * <ul> | |
| * <li>The type can be explicitly specified via the {@link #type()} | |
| * attribute</li> | |
| * <li>If not specified, it will be automatically inferred from the bean's | |
| * interfaces or superclass</li> | |
| * <li>If the bean implements multiple interfaces, the type must be | |
| * explicitly specified to avoid ambiguity</li> | |
| * <li>If the bean doesn't implement any interface or extend any class (other | |
| * than Object), the bean's own class will be used as the type</li> | |
| * </ul> | |
| * <p> | |
| * <strong>Client Mapping:</strong> | |
| * A single bean can be mapped to multiple clients by providing an array of | |
| * client | |
| * identifiers in the {@link #value()} attribute. This is useful when multiple | |
| * clients share the same type implementation. | |
| * <p> | |
| * <strong>Example usage:</strong> | |
| * | |
| * <pre> | |
| * {@code | |
| * // Simple case: single client, type inferred from interface | |
| * @Client("clientA") | |
| * @Service | |
| * public class ClientAPaymentService implements PaymentService {...} | |
| * | |
| * // Explicit type specification | |
| * @Client(value = "clientB", type = PaymentService.class) | |
| * @Service | |
| * public class ClientBPaymentService implements PaymentService {...} | |
| * | |
| * // Multiple clients sharing the same implementation | |
| * @Client( { "clientC", "clientD" } ) | |
| * @Service | |
| * public class SharedPaymentService implements PaymentService {...} | |
| * } | |
| * </pre> | |
| * | |
| * <p> | |
| * <strong>Requirements:</strong> | |
| * <ul> | |
| * <li>Beans must be Spring-managed (annotated with {@code @Service}, | |
| * {@code @Component}, etc.)</li> | |
| * <li>Beans must be singleton scope (prototype scope is not supported)</li> | |
| * <li>Client identifiers must be non-empty strings</li> | |
| * <li>Each client-type combination must be unique (no duplicates | |
| * allowed)</li> | |
| * </ul> | |
| * | |
| * @see ClientRegistry | |
| * @author Panta Dattatreya Reddy | |
| */ | |
| @Documented | |
| @Target(ElementType.TYPE) | |
| @Retention(RetentionPolicy.RUNTIME) | |
| public @interface Client { | |
| /** | |
| * The client identifier(s) for this bean. | |
| * This value is used to map the bean to one or more specific clients. | |
| * <p> | |
| * A single bean can be mapped to multiple clients by providing multiple | |
| * identifiers. This is useful when multiple clients share the same bean | |
| * implementation. | |
| * <p> | |
| * <strong>Example:</strong> | |
| * | |
| * <pre> | |
| * {@code | |
| * | |
| * @Client( { "clientA", "clientB" } ) // Maps to both clientA and clientB | |
| * @Service | |
| * public class SharedService implements PaymentService {...} | |
| * } | |
| * </pre> | |
| * | |
| * @return array of client identifiers (must be non-empty) | |
| */ | |
| String[] value(); | |
| /** | |
| * The class type that this bean implements or extends. | |
| * <p> | |
| * If not specified (defaults to {@code Void.class}), the registry will attempt | |
| * to automatically determine the type using the following rules: | |
| * <ol> | |
| * <li>If the bean implements exactly one interface, that interface is used</li> | |
| * <li>If the bean implements multiple interfaces, an exception is thrown | |
| * (explicit specification required)</li> | |
| * <li>If the bean implements no interfaces, the superclass (if not Object) | |
| * is used</li> | |
| * <li>If none of the above apply, the bean's own class is used as the type</li> | |
| * </ol> | |
| * <p> | |
| * <strong>When to specify explicitly:</strong> | |
| * <ul> | |
| * <li>Bean implements multiple interfaces</li> | |
| * <li>You want to be explicit about the contract</li> | |
| * </ul> | |
| * | |
| * @return the type of bean, or {@code Void.class} if not specified | |
| */ | |
| Class<?> type() default Void.class; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import org.apache.commons.lang3.StringUtils; | |
| import org.jetbrains.annotations.NotNull; | |
| import org.springframework.aop.support.AopUtils; | |
| import org.springframework.beans.BeansException; | |
| import org.springframework.beans.factory.InitializingBean; | |
| import org.springframework.beans.factory.ListableBeanFactory; | |
| import org.springframework.beans.factory.NoSuchBeanDefinitionException; | |
| import org.springframework.context.ApplicationContext; | |
| import org.springframework.context.ApplicationContextAware; | |
| import org.springframework.stereotype.Component; | |
| import java.util.*; | |
| /** | |
| * Registry for retrieving client-specific bean implementations. | |
| * <p> | |
| * This registry provides a mechanism to retrieve beans based on both | |
| * the type and a client identifier. It automatically discovers and | |
| * registers all beans annotated with {@link Client} during Spring | |
| * initialization. | |
| * <p> | |
| * <strong>Architecture:</strong> | |
| * The registry maintains an internal immutable map structure: | |
| * <p> | |
| * {@code Map<Class<Type>, Map<ClientId, BeanInstance>>} | |
| * <p> | |
| * This allows efficient O(1) lookup of beans by client and type. | |
| * <p> | |
| * <strong>Usage Example:</strong> | |
| * | |
| * <pre>{@code | |
| * @Autowired | |
| * private ClientRegistry clientRegistry; | |
| * | |
| * public void processPayment(String clientId) { | |
| * PaymentService paymentService = clientRegistry.getBean(clientId, PaymentService.class); | |
| * paymentService.process(); | |
| * } | |
| * }</pre> | |
| * <p> | |
| * <strong>Thread Safety:</strong> | |
| * This registry is thread-safe. The internal maps are immutable after | |
| * initialization, and all bean lookups are read-only operations. | |
| * <p> | |
| * <strong>Initialization:</strong> | |
| * The registry initializes during Spring's bean lifecycle: | |
| * <ol> | |
| * <li>{@link #setApplicationContext(ApplicationContext)} is called to | |
| * provide access to the Spring context</li> | |
| * <li>{@link #afterPropertiesSet()} is called after dependency injection, | |
| * which triggers the discovery and registration of all | |
| * {@code @Client} annotated beans</li> | |
| * </ol> | |
| * <p> | |
| * <strong>Error Handling:</strong> | |
| * The registry performs comprehensive validation during initialization: | |
| * <ul> | |
| * <li>Validates that beans are singleton scope (prototypes not supported)</li> | |
| * <li>Validates client identifiers are non-empty</li> | |
| * <li>Detects and prevents duplicate client-type mappings</li> | |
| * <li>Validates type compatibility between beans and declared types</li> | |
| * <li>Handles ambiguous types (multiple interfaces) with clear error messages</li> | |
| * </ul> | |
| * | |
| * @see Client | |
| * @author Panta Dattatreya Reddy | |
| */ | |
| @Component | |
| public class ClientRegistry implements ApplicationContextAware, InitializingBean { | |
| /** | |
| * Internal immutable map structure for efficient bean lookup. | |
| * <p> | |
| * Structure: {@code Map<Class<Type>, Map<ClientId, BeanInstance>>} | |
| * <p> | |
| * This map is populated during initialization and remains immutable | |
| * thereafter for thread safety. All beans must be singleton scope. | |
| */ | |
| private Map<Class<?>, Map<String, Object>> typeBeanMap = new HashMap<>(); | |
| private ListableBeanFactory beanFactory; | |
| /** | |
| * Retrieves a bean for the specified client and type. | |
| * <p> | |
| * This method performs a lookup in the internal map structure to find the | |
| * appropriate type implementation for the given client. The lookup is | |
| * thread-safe and efficient (O(1) complexity). | |
| * <p> | |
| * <strong>Example:</strong> | |
| * | |
| * <pre>{@code | |
| * PaymentService service = registry.getBean("clientA", PaymentService.class); | |
| * }</pre> | |
| * | |
| * @param client the client identifier (must be non-null and non-empty) | |
| * @param type the type class (must be non-null) | |
| * @param <Type> the type | |
| * @return the bean instance (never null) | |
| * @throws IllegalArgumentException if the client identifier is null or | |
| * empty, or if type is null | |
| * @throws NoSuchBeanDefinitionException if no bean is found for the specified | |
| * client and type combination | |
| */ | |
| public <Type> @NotNull Type getBean(String client, Class<Type> type) { | |
| if (StringUtils.isBlank(client)) { | |
| throw new IllegalArgumentException("Client identifier cannot be null or empty"); | |
| } | |
| if (type == null) { | |
| throw new IllegalArgumentException("Type cannot be null. Please provide a valid bean type."); | |
| } | |
| var bean = Optional.ofNullable(typeBeanMap.get(type)) | |
| .map(clientMap -> clientMap.get(client)) | |
| .orElseThrow(() -> new NoSuchBeanDefinitionException( | |
| String.format("No bean found for client '%s' and type: %s.class", | |
| client, type.getSimpleName()))); | |
| // The cast is now safe because we validated type compatibility during init | |
| return type.cast(bean); | |
| } | |
| /** | |
| * Initializes the type bean map after all properties have been set. | |
| * <p> | |
| * This method is called by Spring after dependency injection is complete | |
| * (as part of the {@link InitializingBean} lifecycle). It performs the | |
| * following operations: | |
| * <ol> | |
| * <li>Scans the application context for all beans annotated with | |
| * {@link Client}</li> | |
| * <li>Validates bean configurations (scope, client identifiers, type | |
| * compatibility)</li> | |
| * <li>Builds an immutable map structure for efficient bean retrieval</li> | |
| * </ol> | |
| * <p> | |
| * After this method completes, the registry is ready to serve bean lookup | |
| * requests. | |
| * | |
| * @throws IllegalStateException if duplicate client-type mappings are | |
| * found, ambiguous types are detected, | |
| * or beans have invalid configurations | |
| * @throws IllegalArgumentException if prototype beans are found or validation | |
| * fails | |
| */ | |
| @Override | |
| public void afterPropertiesSet() { | |
| initializeTypeBeanMap(); | |
| } | |
| /** | |
| * Sets the Spring ApplicationContext for this registry. | |
| * <p> | |
| * This method is called by Spring during bean initialization (as part of the | |
| * {@link ApplicationContextAware} interface) to provide access to the | |
| * application context. The context is used to discover and access beans | |
| * annotated with {@link Client}. | |
| * <p> | |
| * The context is stored as a {@link ListableBeanFactory} to limit the scope | |
| * of operations to only what is needed for bean discovery. | |
| * | |
| * @param applicationContext the Spring ApplicationContext instance (never null) | |
| * @throws BeansException if an error occurs while setting the context | |
| */ | |
| @Override | |
| public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException { | |
| // ApplicationContext extends ListableBeanFactory, so this assignment is valid | |
| // using ListableBeanFactory to avoid unnecessary scope expansion | |
| this.beanFactory = applicationContext; | |
| } | |
| /** | |
| * Initializes the internal type bean map by scanning all beans annotated | |
| * with {@link Client}. | |
| * <p> | |
| * This method performs the following operations: | |
| * <ol> | |
| * <li><strong>Discovery:</strong> Finds all beans with the | |
| * {@code @Client} annotation using Spring's bean registry</li> | |
| * <li><strong>Scope Validation:</strong> Ensures all beans are singleton | |
| * scope (prototype scope is not supported)</li> | |
| * <li><strong>Type Resolution:</strong> Determines the type | |
| * from the annotation's {@code type} attribute, or infers it from | |
| * the bean's interfaces or superclass</li> | |
| * <li><strong>Validation:</strong> Validates client identifiers are | |
| * non-empty and checks type compatibility</li> | |
| * <li><strong>Duplicate Detection:</strong> Ensures no duplicate | |
| * client-type mappings exist</li> | |
| * <li><strong>Map Construction:</strong> Builds an immutable map structure | |
| * for efficient O(1) bean lookup</li> | |
| * </ol> | |
| * <p> | |
| * The resulting map structure is: | |
| * {@code Map<Type, Map<ClientId, BeanInstance>>} | |
| * <p> | |
| * * All maps are made unmodifiable for thread safety. | |
| * | |
| * @throws IllegalArgumentException if prototype beans are found, client | |
| * identifiers are empty, or validation fails | |
| * @throws IllegalStateException if duplicate client-type mappings exist, | |
| * ambiguous types are detected | |
| * (multiple interfaces), or beans don't | |
| * implement/extend the specified type | |
| */ | |
| private void initializeTypeBeanMap() { | |
| var beansWithAnnotation = beanFactory.getBeansWithAnnotation(Client.class); | |
| var tempMap = new HashMap<Class<?>, Map<String, Object>>(); | |
| beansWithAnnotation.forEach((beanName, beanInstance) -> { | |
| if (beanFactory.isPrototype(beanName)) { | |
| throw new IllegalArgumentException(String.format( | |
| "Bean %s.class is configured as a prototype scope, which is not supported. " + | |
| "@Client annotated beans must be singleton scope.", | |
| beanInstance.getClass().getSimpleName())); | |
| } | |
| var annotation = beanFactory.findAnnotationOnBean(beanName, Client.class); | |
| // Defensive check (though getBeansWithAnnotation usually guarantees presence) | |
| if (annotation == null) | |
| return; | |
| var clients = annotation.value(); | |
| validateAnnotation(beanInstance, clients); | |
| var type = getType(beanInstance, annotation); | |
| validateTypeCompatibility(beanInstance, type); | |
| // Compute inner map | |
| var clientMap = tempMap.computeIfAbsent(type, k -> new HashMap<>()); | |
| for (var client : clients) | |
| addBeanToClientMap(beanInstance, client, clientMap, type); | |
| }); | |
| // Make maps unmodifiable for thread safety/immutability | |
| tempMap.replaceAll((k, v) -> Collections.unmodifiableMap(v)); | |
| this.typeBeanMap = Collections.unmodifiableMap(tempMap); | |
| } | |
| /** | |
| * Adds a bean instance to the client map, performing duplicate detection. | |
| * <p> | |
| * This method ensures that each client-type combination is unique. If a | |
| * duplicate mapping is detected, an exception is thrown with detailed | |
| * information about the conflicting beans. | |
| * | |
| * @param beanInstance the bean instance to add | |
| * @param client the client identifier | |
| * @param clientMap the map of client IDs to bean instances for this type | |
| * @param type the type class | |
| * @throws IllegalStateException if a duplicate client-type mapping is | |
| * detected | |
| */ | |
| private void addBeanToClientMap(Object beanInstance, String client, Map<String, Object> clientMap, Class<?> type) { | |
| // DUPLICATE CHECK: Fail fast if two beans define the same client+type | |
| if (clientMap.containsKey(client)) { | |
| var existingBean = clientMap.get(client); | |
| throw new IllegalStateException( | |
| String.format("Duplicate @Client definition detected. " + | |
| "Client '%s' for type %s.class is already mapped to bean class %s.class. " + | |
| "Conflicting bean: %s.class. Each client-type combination must be unique.", | |
| client, type.getSimpleName(), | |
| existingBean.getClass().getSimpleName(), | |
| beanInstance.getClass().getSimpleName())); | |
| } | |
| clientMap.put(client, beanInstance); | |
| } | |
| /** | |
| * Determines the type for a bean instance. | |
| * <p> | |
| * This method resolves the type using the following priority: | |
| * <ol> | |
| * <li>If explicitly specified in the annotation's {@code type} attribute | |
| * (and not {@code Void.class}), use that value</li> | |
| * <li>If the bean implements exactly one interface, use that interface</li> | |
| * <li>If the bean implements multiple interfaces, throw an exception | |
| * (ambiguity)</li> | |
| * <li>If the bean has a superclass (other than Object), use that | |
| * superclass</li> | |
| * <li>Otherwise, use the bean's own class as the type</li> | |
| * </ol> | |
| * <p> | |
| * This method handles AOP proxies correctly by using | |
| * {@link AopUtils#getTargetClass(Object)} to unwrap proxy objects. | |
| * | |
| * @param beanInstance the bean instance to analyze | |
| * @param annotation the {@link Client} annotation from the bean | |
| * @return the resolved type class (never null) | |
| * @throws IllegalStateException if the type cannot be determined | |
| * (ambiguous - multiple interfaces) | |
| */ | |
| private static Class<?> getType(Object beanInstance, Client annotation) { | |
| var type = annotation.type(); | |
| // If type is null, extract immediate interface or class of the annotated | |
| // class | |
| if (type == null || type == Void.class) { | |
| var targetClass = AopUtils.getTargetClass(beanInstance); | |
| var interfaces = targetClass.getInterfaces(); | |
| if (interfaces.length == 1) { | |
| type = interfaces[0]; | |
| } else if (interfaces.length > 1) { | |
| // AMBIGUITY CHECK | |
| throw new IllegalStateException(String.format( | |
| "Bean %s.class implements multiple interfaces %s, making the type ambiguous. " + | |
| "Please explicitly specify the target type using the 'type' attribute " + | |
| "in the @Client annotation " + | |
| "(e.g., @Client(value = \"%s\", type = %s.class)).", | |
| beanInstance.getClass().getSimpleName(), Arrays.toString(interfaces), | |
| Arrays.toString(annotation.value()), interfaces[0].getSimpleName())); | |
| } else { | |
| // Otherwise use the parent class (superclass) | |
| var superClass = targetClass.getSuperclass(); | |
| if (superClass != null && superClass != Object.class) { | |
| type = superClass; | |
| } else { | |
| // If no interface or superclass found, use the bean's own class as the type | |
| type = targetClass; | |
| } | |
| } | |
| } | |
| return type; | |
| } | |
| /** | |
| * Validates that the client identifier(s) in the {@link Client} | |
| * annotation are not null or empty. | |
| * <p> | |
| * This method ensures that: | |
| * <ul> | |
| * <li>The clients array is not empty</li> | |
| * <li>Each client identifier in the array is not null or blank</li> | |
| * </ul> | |
| * | |
| * @param beanInstance the bean instance being validated (used for error | |
| * messages) | |
| * @param clients the client identifiers from the annotation | |
| * @throws IllegalStateException if the clients array is empty or contains any | |
| * null or blank identifiers | |
| */ | |
| private void validateAnnotation(Object beanInstance, String[] clients) { | |
| // Cache class name to avoid repeated method calls | |
| var errorMessage = String.format( | |
| "Bean %s.class has @Client annotation with empty client identifier. " + | |
| "The 'value' attribute must specify a non-empty client identifier.", | |
| beanInstance.getClass().getSimpleName()); | |
| if (clients.length == 0) { | |
| throw new IllegalStateException(errorMessage); | |
| } | |
| for (var client : clients) { | |
| if (StringUtils.isBlank(client)) { | |
| throw new IllegalStateException(errorMessage); | |
| } | |
| } | |
| } | |
| /** | |
| * Validates that the bean instance actually implements or extends the | |
| * type declared in the annotation. | |
| * <p> | |
| * This method uses {@link AopUtils#getTargetClass(Object)} to handle AOP | |
| * proxies correctly. It unwraps CGLIB/JDK proxies to check the actual | |
| * underlying class and its interfaces. | |
| * <p> | |
| * The validation ensures type safety by verifying that the bean can be safely | |
| * cast to the declared type. | |
| * | |
| * @param beanInstance the bean instance to validate | |
| * @param type the type class that the bean should implement or extend | |
| * @throws IllegalStateException if the bean does not implement or extend the | |
| * specified type | |
| */ | |
| private void validateTypeCompatibility(Object beanInstance, Class<?> type) { | |
| // AopUtils.getTargetClass unwraps CGLIB/JDK proxies to check the actual | |
| // underlying class/interfaces | |
| var targetClass = AopUtils.getTargetClass(beanInstance); | |
| if (!type.isAssignableFrom(targetClass)) { | |
| throw new IllegalStateException( | |
| String.format("Bean %s.class is annotated with type %s.class " | |
| + "but does not implement or extend it. " | |
| + "Please ensure the bean class implements the specified " | |
| + "interface or extends the specified class type.", | |
| beanInstance.getClass().getSimpleName(), type.getSimpleName())); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment