arakoodev / FlySpring

Developer-friendly Spring Boot flavor. Supercharged cli "flyfly" to make you fall in love with Java again
MIT License
7 stars 7 forks source link

fastapi style dependency injection #48

Open sandys opened 1 year ago

sandys commented 1 year ago

https://fastapi.tiangolo.com/tutorial/dependencies/

create a dependency injection system for spring webflux, that is similar to the dependency injection in Fastapi (https://fastapi.tiangolo.com/tutorial/dependencies/)

Dependency injection in Spring Webflux can be achieved using the Spring framework's built-in dependency injection features. Here's a guide on how you can create a dependency injection system similar to FastAPI.

  1. Create a Maven or Gradle project and add the necessary dependencies for Spring Webflux. For example, in a Maven project, add the following dependencies to your pom.xml:
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>
  1. Create a simple service class that you want to inject as a dependency. For example, let's create a GreetingService:
@Service
public class GreetingService {

  public String greet(String name) {
    return "Hello, " + name + "!";
  }
}
  1. Create a controller class that uses the GreetingService as a dependency:
@RestController
public class GreetingController {

  private final GreetingService greetingService;

  // Constructor-based dependency injection
  public GreetingController(GreetingService greetingService) {
    this.greetingService = greetingService;
  }

  @GetMapping("/greet/{name}")
  public String greet(@PathVariable String name) {
    return greetingService.greet(name);
  }
}
  1. Create a Spring Boot application class to run your application:
@SpringBootApplication
public class WebfluxDependencyInjectionApplication {

  public static void main(String[] args) {
    SpringApplication.run(WebfluxDependencyInjectionApplication.class, args);
  }
}
  1. Run your application and test the endpoint by accessing http://localhost:8080/greet/John.

In this example, we created a GreetingService and injected it into the GreetingController using constructor-based dependency injection. The @Service annotation tells Spring that the GreetingService is a service class that should be managed by the Spring container. The @RestController annotation indicates that the GreetingController class is a REST controller managed by the Spring container.

This is a simple example of creating a dependency injection system for Spring Webflux. For more complex scenarios, you can also use the @Autowired annotation to inject dependencies using field-based or setter-based dependency injection, or use @Bean annotations to create custom beans and control their lifecycle.

this is not what i want. i want to have a function called "Depends" that will explicitly tell springboot that something has to be injected.

I apologize for the confusion. In that case, you can create a custom Depends class and use it to explicitly specify your dependencies. Here's an example:

  1. Create a custom Depends class:
public class Depends<T> {
    private final Class<T> type;

    public Depends(Class<T> type) {
        this.type = type;
    }

    public Class<T> getType() {
        return type;
    }
}
  1. Create an annotation @DependsOn to mark the method parameter that should be injected:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DependsOn {
}
  1. Create a custom HandlerMethodArgumentResolver to resolve the method arguments marked with @DependsOn:
@Component
public class DependsArgumentResolver implements HandlerMethodArgumentResolver {
    private final ApplicationContext applicationContext;

    public DependsArgumentResolver(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(DependsOn.class) && parameter.getParameterType().equals(Depends.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Depends<?> depends = (Depends<?>) parameter.getParameter().getAnnotation(DependsOn.class).annotationType().getMethod("value").invoke(parameter.getParameterAnnotation(DependsOn.class));
        return applicationContext.getBean(depends.getType());
    }
}
  1. Create a WebFluxConfigurer to register the custom HandlerMethodArgumentResolver:
@Configuration
public class WebFluxConfig implements WebFluxConfigurer {
    private final DependsArgumentResolver dependsArgumentResolver;

    public WebFluxConfig(DependsArgumentResolver dependsArgumentResolver) {
        this.dependsArgumentResolver = dependsArgumentResolver;
    }

    @Override
    public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
        configurer.addCustomResolver(dependsArgumentResolver);
    }
}
  1. Now, you can use the Depends class and @DependsOn annotation in your controller to explicitly specify the dependencies:
@RestController
public class GreetingController {

    @GetMapping("/greet/{name}")
    public String greet(@PathVariable String name, @DependsOn(Depends.class) Depends<GreetingService> greetingServiceDepends) {
        GreetingService greetingService = greetingServiceDepends.getValue();
        return greetingService.greet(name);
    }
}

In this example, we created a Depends class, a custom @DependsOn annotation, a custom HandlerMethodArgumentResolver, and a WebFluxConfigurer to register the custom resolver. Now, when you use the @DependsOn annotation in your controller method, Spring will inject the specified dependency using the Depends class.

Please note that this solution is less idiomatic for Spring and might not be as efficient as using the built-in dependency injection mechanisms. However, it should provide the explicit dependency injection behavior similar to FastAPI.

this is better. however this declaration "@DependsOn(Depends.class) Depends" is too long. can it be aliased or shortened somehow using java language features.

You can make the declaration shorter by creating a custom annotation for each specific dependency. Here's an example:

  1. Create a custom annotation @DependsOnGreetingService:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DependsOnGreetingService {
}
  1. Modify the DependsArgumentResolver to support the new annotation:
@Component
public class DependsArgumentResolver implements HandlerMethodArgumentResolver {
    private final ApplicationContext applicationContext;

    public DependsArgumentResolver(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(DependsOnGreetingService.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        if (parameter.hasParameterAnnotation(DependsOnGreetingService.class)) {
            return applicationContext.getBean(GreetingService.class);
        }
        return null;
    }
}
  1. Now, you can use the @DependsOnGreetingService annotation in your controller to explicitly specify the GreetingService dependency:
@RestController
public class GreetingController {

    @GetMapping("/greet/{name}")
    public String greet(@PathVariable String name, @DependsOnGreetingService GreetingService greetingService) {
        return greetingService.greet(name);
    }
}

In this example, we created a custom @DependsOnGreetingService annotation and modified the DependsArgumentResolver to support it. Now, when you use the @DependsOnGreetingService annotation, Spring will inject the GreetingService dependency.

Please note that this solution requires creating a custom annotation for each specific dependency. If you have many dependencies, this might become cumbersome. However, it shortens the declaration in the controller method and makes it more explicit.

no this is not what i want. can i create a global function called "GetDepends" that will take the service as parameter and automatically create the right annotation?

I apologize for the confusion. Unfortunately, due to the way Java annotations work, you cannot create a global function that creates the right annotation for you. Java annotations require their values to be known at compile time, so you cannot call a function or use a variable to generate the annotation value.

However, you can slightly modify the solution I provided earlier to make the declaration shorter and more readable. Here's a modified version of the solution:

  1. Create a custom Depends class that takes the service class as a type parameter, and add a constructor parameter for the service class:
public class Depends<T> {
    private final Class<T> serviceClass;

    public Depends(Class<T> serviceClass) {
        this.serviceClass = serviceClass;
    }

    public Class<T> getServiceClass() {
        return serviceClass;
    }
}
  1. Modify the DependsArgumentResolver to use the getServiceClass() method:
@Component
public class DependsArgumentResolver implements HandlerMethodArgumentResolver {
    private final ApplicationContext applicationContext;

    public DependsArgumentResolver(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(DependsOn.class) && parameter.getParameterType().equals(Depends.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Depends<?> depends = (Depends<?>) parameter.getParameter().getAnnotation(DependsOn.class).annotationType().getMethod("value").invoke(parameter.getParameterAnnotation(DependsOn.class));
        return applicationContext.getBean(depends.getServiceClass());
    }
}
  1. Create a utility class with a static method GetDepends that returns a new instance of the Depends class with the service class as a parameter:
public class DependsUtil {
    public static <T> Depends<T> GetDepends(Class<T> serviceClass) {
        return new Depends<>(serviceClass);
    }
}
  1. Now, you can use the GetDepends method in your controller to create a Depends instance with the desired service class:
@RestController
public class GreetingController {

    @GetMapping("/greet/{name}")
    public String greet(@PathVariable String name, @DependsOn(DependsUtil.GetDepends(GreetingService.class)) Depends<GreetingService> greetingServiceDepends) {
        GreetingService greetingService = greetingServiceDepends.getValue();
        return greetingService.greet(name);
    }
}

In this solution, we modified the Depends class to take a service class as a constructor parameter and added a utility class with a static GetDepends method that creates a new Depends instance with the service class. This makes the declaration in the controller shorter and more readable.

Please note that this solution still relies on the Depends class and the @DependsOn annotation, but it makes the code more readable by using the GetDepends method to create the Depends instance.

this is better. but why do we need the @dependson annotation ? can we not roll that into the DependsUtil class or getdepends method ?

I apologize for the confusion. You are correct that we can further simplify the solution by removing the @DependsOn annotation and modifying the DependsArgumentResolver to support the Depends class directly. Here's the updated solution:

  1. Remove the @DependsOn annotation.

  2. Modify the DependsArgumentResolver to support the Depends class directly:

@Component
public class DependsArgumentResolver implements HandlerMethodArgumentResolver {
    private final ApplicationContext applicationContext;

    public DependsArgumentResolver(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(Depends.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Depends<?> depends = (Depends<?>) parameter.getParameter().getType().getConstructor(Class.class).newInstance(parameter.getNestedGenericParameterType().getRawClass());
        return applicationContext.getBean(depends.getServiceClass());
    }
}
  1. Now, you can use the GetDepends method in your controller without the @DependsOn annotation:
@RestController
public class GreetingController {

    @GetMapping("/greet/{name}")
    public String greet(@PathVariable String name, Depends<GreetingService> greetingServiceDepends) {
        GreetingService greetingService = greetingServiceDepends.getValue();
        return greetingService.greet(name);
    }
}

In this solution, we removed the @DependsOn annotation and modified the DependsArgumentResolver to support the Depends class directly. This further simplifies the code and allows you to use the Depends class without an additional annotation.

Please note that this solution still relies on the Depends class, but it is now more readable and easier to use. However, keep in mind that it is less idiomatic for Spring, and using built-in dependency injection features is generally recommended.

this is what i want. please make this code better by adding usual safety checks and validations to make it more robust. also log exceptions using logger. and make your code into a spring plugin

To make the code more robust and turn it into a Spring plugin, follow these steps:

  1. Create a new Maven project for the plugin with the following pom.xml (replace your.group.id and your.artifact.id with your desired group and artifact IDs):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>your.group.id</groupId>
    <artifactId>your.artifact.id</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>Spring Depends Plugin</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webflux</artifactId>
            <version>5.3.13</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.32</version>
        </dependency>
    </dependencies>
</project>
  1. Add the Depends class to the plugin:
public class Depends<T> {
    private final Class<T> serviceClass;

    public Depends(Class<T> serviceClass) {
        this.serviceClass = serviceClass;
    }

    public Class<T> getServiceClass() {
        return serviceClass;
    }
}
  1. Add the DependsUtil class to the plugin:
public class DependsUtil {
    public static <T> Depends<T> GetDepends(Class<T> serviceClass) {
        return new Depends<>(serviceClass);
    }
}
  1. Add the DependsArgumentResolver class to the plugin with additional safety checks, validations, and logging:
@Component
public class DependsArgumentResolver implements HandlerMethodArgumentResolver {
    private final ApplicationContext applicationContext;
    private final Logger logger = LoggerFactory.getLogger(DependsArgumentResolver.class);

    public DependsArgumentResolver(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(Depends.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        if (!supportsParameter(parameter)) {
            logger.error("Unsupported parameter type: {}", parameter.getParameterType());
            return null;
        }

        try {
            Depends<?> depends = (Depends<?>) parameter.getParameter().getType().getConstructor(Class.class).newInstance(parameter.getNestedGenericParameterType().getRawClass());
            return applicationContext.getBean(depends.getServiceClass());
        } catch (Exception e) {
            logger.error("Failed to resolve argument: {}", e.getMessage(), e);
            throw e;
        }
    }
}
  1. Create an auto-configuration class for the plugin:
@Configuration
public class DependsAutoConfiguration implements WebFluxConfigurer {
    private final DependsArgumentResolver dependsArgumentResolver;

    public DependsAutoConfiguration(DependsArgumentResolver dependsArgumentResolver) {
        this.dependsArgumentResolver = dependsArgumentResolver;
    }

    @Override
    public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
        configurer.addCustomResolver(dependsArgumentResolver);
    }
}
  1. Create a spring.factories file in the src/main/resources/META-INF folder with the following content to enable auto-configuration for the plugin:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=your.group.id.DependsAutoConfiguration
  1. Build your plugin using mvn clean install.

Now, to use the plugin in a Spring Boot project, add the following dependency to your pom.xml (replace your.group.id and your.artifact.id with your actual group and artifact IDs):

<dependency>
    <groupId>your.group.id</groupId>
    <artifactId>your.artifact.id</artifactId>
    <version>1.0.0</version>
</dependency>

Then, you can use the Depends class and the GetDepends method in your controller:

@RestController
public class GreetingController {

    @GetMapping("/greet/{name}")
    public String greet(@PathVariable String name, Depends<GreetingService> greetingServiceDepends) {
        GreetingService greetingService = greetingServiceDepends.getValue();
        return greetingService.greet(name);
    }
}

This plugin provides a more robust implementation of the Depends functionality with additional safety checks, validations, and logging. However, please note that this solution is still less idiomatic for Spring and might not be as efficient as using the built-in dependency injection features.