Spring Boot webflux 应用程序中基于文件(或基于包)的 RouterFunction

San*_*eep 5 java spring spring-mvc spring-boot spring-webflux

我们在内部公司框架中想要的东西之一是 Spring Boot 应用程序中基于文件路径或包的路由。幸运的是,在 java 中,文件和包是相同的......所以两者都可以。

例如,我想强制org.company.api从其包名称中选取下面的所有包。org.company.api.user所以will be/userorg.company.api.user.loginwill be中的控制器/user/login。注意:记住org.company因项目而异。基本上我们希望 api 目录(相当于 api 子包)下的所有内容都用于此目的。

我想我可以使用 webflux 路由器功能来实现这一点,但我不确定是否有更好的方法。如何为目录下的所有文件生成路由器功能?

Ken*_*han 3

如果我正确理解您的问题,您希望自动向每个控制器方法的 URL 添加路径前缀,并且路径前缀应从控制器的包名称派生。

您可以通过以下方式做到这一点,但它需要使用作为输入参数和输出来RouterFunction实现控制器方法(即HandlerFunction) 。因此,如果你想将控制器方法实现为普通的java方法,可以使用任何类型作为输入和输出,你需要实现一些代码来在 / 之间转换为任何输入和输出类型,以适应ServerRequestServerResponseServerRequestServerResponseHandlerFunction可以调用任何普通的方法。然后,您最终会发现您正在重新发明 spring-mvc 带注释的控制器方法已经完成的事情。

实际上 spring-mvc 已经允许使用自定义控制器方法的路径前缀PathMatchConfigurer(请参阅此处的文档)。但使用PathMatchConfigurer是相当静态的,需要您手动维护所有路径前缀。因此,为了更加动态地根据控制器的包自动配置所有路径前缀,我们需要另一种方法来做到这一点。

在幕后PathMatchConfigurer所做的只是在实例化之前配置pathPrefixes内部。RequestMappingHandlerMapping因此,我们需要一种在实例化之前配置它的方法RequestMappingHandlerMapping,但在我们有足够的信息来了解所有路径前缀之后,我们需要配置在我们知道 spring 将创建哪些控制器 bean 之后发生的情况。

BeanFactoryPostProcessor正是提供了这样的钩子,它允许在实例化任何bean之前但在BeanDefinition收集所有bean元数据(即)之后执行代码。所以我们可以实现一个BeanFactoryPostProcessor它的工作是配置这个pathPrefixes地图RequestMappingHandlerMapping通过找出包含控制器的所有包来

以下内容BeanFactoryPostProcessor应该是您的一个很好的起点。

    public class PathPrefixFactoryPostProcessor implements BeanFactoryPostProcessor {

        private final String rootPackage;

        public FooBeanFactoryPostProcessor(String rootPackage) {
            this.rootPackage = rootPackage;
        }

        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

            List<String> controllerPackageNames = findPackageContainController(beanFactory);

            Map<String, Predicate<Class<?>>> pathPrefixesMap = new HashMap<>();
            for (String packageName : controllerPackageNames) {
                if (packageName.startsWith(rootPackage)) {
                    String pathPrefix = packageName.substring(rootPackage.length());
                    pathPrefix = pathPrefix.replace(".", "/");
                    pathPrefixesMap.put(pathPrefix, HandlerTypePredicate.forBasePackage(packageName));
                }

            }
            if(!pathPrefixesMap.isEmpty()){
               BeanDefinition def = beanFactory.getBeanDefinition("requestMappingHandlerMapping");
               def.getPropertyValues().add("pathPrefixes", pathPrefixesMap);
            }
        }

        private List<String> findPackageContainController(ConfigurableListableBeanFactory beanFactory) {
            List<String> result = new ArrayList<>();
            for (String bdName : beanFactory.getBeanDefinitionNames()) {
                BeanDefinition bd = beanFactory.getBeanDefinition(bdName);

                Class<?> type = bd.getResolvableType().resolve();
                if (type != null) {
                    if (AnnotatedElementUtils.hasAnnotation(type, Controller.class) ||
                            AnnotatedElementUtils.hasAnnotation(type, RequestMapping.class)) {
                        result.add(type.getPackageName());
                    }
                }
            }
            return result;
        }
    }
Run Code Online (Sandbox Code Playgroud)
  • 假设您配置的RequestMappingHandlerMappingbean具有名称requestMappingHandlerMapping(默认情况下应该如此)
  • 如果包含控制器类的包不以 开头org.company.api,则不会添加路径前缀。

然后将其定义为 bean :

@Configuraiton
public class Config {
    
    @Bean
    public PathPrefixFactoryPostProcessor pathPrefixFactoryPostProcessor(){
        return new PathPrefixFactoryPostProcessor("org.company.api");
    }
}
Run Code Online (Sandbox Code Playgroud)

然后给定两个包org.company.api.user并且org.company.api.user.login包包含以下控制器

@RestController
public class FooController {
    
   @GetMapping("/foo")
   public String foo(){

   }
}
Run Code Online (Sandbox Code Playgroud)

foo()映射到

  • /user/foo对于包裹org.company.api.user
  • /user/login/foo对于包裹org.company.api.user.login