Create different instances of a class chain based on a field in the input request with Spring Boot

Viewed 405

Every request that my Java application receives, passes through 4 layers:

Handler --> Validator --> Processor --> DAO
  • Handler is the API Resource. (Handler.java)
  • Validator validates the input. (Validator.java)
  • Processor performs some business logic. (Processor.java)
  • DAO is the DB communication layer. (DAO.java)

The input request has a field called the request_type. Based on this request_type, I want to create different objects for all the layer classes, i.e:

request_type_A should pass through Handler1, Validator1, Processor1, DAO1 (instances)
request_type_B should pass through Handler2, Validator2, Processor2, DAO2 (instances)
request_type_C should pass through Handler3, Validator3, Processor3, DAO3 (instances).. and so on

To clarify, the requirement is to create different chain of objects for a given request type, so that two request having different request_type have entirely different object chain instances. Basically i want to shard my application's object based on a given request_type.

I am using Spring Boot. Is there a way that spring's ApplicationContext can provide different object chains for different object types. Or should I manage these instances by my own?

Is there a way I can create a library which would give me a new object instance for every layer, based on the request_type using Spring's ApplicationContext?

Or should i create multiple ApplicationContext?

3 Answers

Based on comments & question, I understand that you would be receiving 2 or 3 request_type.

So main idea which I have used here is to use constructor injection of chained objects with different configuration beans which will be used based on your request type.

Feel free to check-out this simple demonstration based code from github where I have proposed my idea : https://github.com/patilashish312/SpringObjectChaining

So based on this code, I can confirm that

  1. This application is not creating chain of object per request but will re-use if same type of requests received by application
  2. Objects assigned to one request type is not being used by other request type.

Below console output is proof :

displaying request MyRequest(id=1, name=Ashish, requestType=requestTypeA)
Printing handler bean com.spr.boot3.ConditionalVerification.Handler.MyHandler@31182e0a
Printing validator bean com.spr.boot3.ConditionalVerification.Validator.MyValidator@484e3fe7
Printing processor bean com.spr.boot3.ConditionalVerification.Processor.MyProcessor@70f9b9c7
Printing dao bean com.spr.boot3.ConditionalVerification.Dao.MyDao@2a8175d9
inside dao, doing DAO processing
displaying request MyRequest(id=1, name=Ashish, requestType=requestTypeA)
Printing handler bean com.spr.boot3.ConditionalVerification.Handler.MyHandler@31182e0a
Printing validator bean com.spr.boot3.ConditionalVerification.Validator.MyValidator@484e3fe7
Printing processor bean com.spr.boot3.ConditionalVerification.Processor.MyProcessor@70f9b9c7
Printing dao bean com.spr.boot3.ConditionalVerification.Dao.MyDao@2a8175d9
inside dao, doing DAO processing
displaying request MyRequest(id=1, name=Ashish, requestType=requestTypeB)
Printing handler bean com.spr.boot3.ConditionalVerification.Handler.MyHandler@55ea9008
Printing validator bean com.spr.boot3.ConditionalVerification.Validator.MyValidator@5b2d74c5
Printing processor bean com.spr.boot3.ConditionalVerification.Processor.MyProcessor@5f12fb78
Printing dao bean com.spr.boot3.ConditionalVerification.Dao.MyDao@1a107efe
inside dao, doing DAO processing
displaying request MyRequest(id=1, name=Ashish, requestType=requestTypeB)
Printing handler bean com.spr.boot3.ConditionalVerification.Handler.MyHandler@55ea9008
Printing validator bean com.spr.boot3.ConditionalVerification.Validator.MyValidator@5b2d74c5
Printing processor bean com.spr.boot3.ConditionalVerification.Processor.MyProcessor@5f12fb78
Printing dao bean com.spr.boot3.ConditionalVerification.Dao.MyDao@1a107efe
inside dao, doing DAO processing

I had a similar requirement in my solution. What I built was a general-purpose command handler, and used a decorator pattern of annotations on each command to provide the specification for which handlers, validators, processors, and dao.

In my implementation, I have API handlers which convert requests to specific commands. Command class was an subclass of an abstract command class with a generic type param.

API -> all API variables are copied into a wrapper data model. (This could encapsulate the entrypoint of your handler concept or request_type concept)

Command extends AbstractCommand where T is the wrapper data model.

Then I would have an annotation for each of your concepts: Handler, Validator, Processor, Dao.

The general purpose command handler would have a method that "process"es commands by reading their annotations and then lining up the annotation helper that corresponds to that annotation. This could use the application context to load the bean of the class referenced in the annotation value. By providing a sequencing property for each of the annotation helpers you could loop over the sorted helpers to perform actions in the right order.

In my implementation this was further augmented by whether or not the command included asynchronous behavior, so that all the synchronous behavior would occur first, and the asychronous behavior would be wrapped in a background thread.

The beans that are injected in the rest controller don't vary with the http request content. What you can do is factor your request_type as a path variable and create the desired chains in separate http mappings like so:

    @PostMapping(value = "/request_type_A")
    public Object handle1(args...){
        // Validator1 --> Processor1 --> DAO1
    }
    
    
    @PostMapping(value = "/request_type_B")
    public Object handle2(args...){
        // Validator2 --> Processor2 --> DAO2
    }

If this is not practical for whatever reason and you must specify the type dynamically in the @RequestBody, @PathVariable or @RequestParam, then I would suggest implementing a resolver bean similar to this:

@Component
public class Resolver {

    private final RequestTypeAValidator requestTypeAValidator;
    private final RequestTypeBValidator requestTypeBValidator;
    
    ...
    
    public IValidator getValidator(String requestType) {
        switch (requestType) {
            case "request_type_A":
                return requestTypeAValidator;
            case "request_type_B":
                return requestTypeBValidator;
            default: 
                throw new IllegalArgumentException("cannot find validator");
        }
    }
}

The drawback of this approach is that it does not comply with the "Open-Closed" principle in the sense that for any new request type, you will need to edit the resolvers. That can be fixed by using a HashMap in the resolver and letting the beans register themselves to that map on @PostConstruct:

@Component
public class Resolver {

    private final Map<String, IValidator> validators = new HashMap<>();

    public IValidator getValidator(String requestType) {
        IValidator result = validators.get(requestType);
        if (Objects.isNull(result)) {
            throw new IllegalArgumentException("cannot find validator");
        }
        return result;
    }
    
    public void register(String type, IValidator validator) {
        validators.put(type, validator)
    }
}
@Component
public class ValidatorA implements IValidator {

    private final Resolver resolver;

    @PostConstruct
    private void register() {
        resolver.register("request_type_A", this);
    }

    ...
}

However, in this approach there is a direct dependency from all implementations back to the Resolver.

Lastly, you could inject dynamically like so:

@Component
public class Resolver {

    private final ApplicationContext applicationContext;
    ...

    public IValidator getValidator(String requestType) {
        switch (requestType) {
            case "request_type_A":
                try {
                    return applicationContext.getBean(ValidatorA.class);
                } catch (NoSuchBeanDefinitionException e) {
                    // handle exception
                }
            case "request_type_B":
                try {
                    return applicationContext.getBean(ValidatorB.class);
                } catch (NoSuchBeanDefinitionException e) {
                    // handle exception
                }
            default:
                throw new IllegalArgumentException("cannot find validator");
        }
    }
}

Note: Avoid taking the client specified string as the class name or type directly in the applicationContext.getBean() call. That is not safe and may present a great security vulnerability, use a switch or dictionary to resolve the correct bean name or bean type.

If you want to inject multiple instances of the same classes, create a configuration class and declare the beans like this:

@Configuration
public class BeanConfiguration {
    
    @Bean
    public IValidator aValidator(){
        return new ValidatorImpl(...);
    }

    @Bean
    public IValidator bValidator(){
        return new ValidatorImpl(...);
    }
}

And then to inject it, you can either use the dynamic resolution by name as above, or use the @Qualifier annotation:

@Service
public class MyService {

    private final ApplicationContext applicationContext;

    private final IValidator bValidator;

    public MyService(ApplicationContext applicationContext, @Qualifier("bValidator") IValidator bValidator) {
        this.applicationContext = applicationContext;
        this.bValidator = bValidator;
    }

    public void getDynamically(){
        IValidator aValidator = (IValidator)applicationContext.getBean("aValidator");
    }
}
Related