如何从属性文件创建动态 Spring boot RestControllers

问题描述 投票:0回答:3

背景

您好,我是 Spring 新手,正在尝试使用提供 HTTP 请求的 Spring Boot 应用程序。

我有几个具有不同 URI 的“客户”端点。

我想在运行时或容器启动期间动态创建其余控制器。我知道我可以利用 @Profile 来实现这一点,但需要的是,我需要将这些动态 URI 替换为相应的静态 URI(以清理通过用于模拟的wiremock记录的映射)。

例如-

  1. 我可能会收到 /users/1/ID 的请求
  2. 我需要将此网址清理为“/users/ID”
  3. 每个客户都有大量此类 URI
  4. 我可以动态创建这些控制器,而不是手动编辑和操作它们吗?

我计划创建一个 RestController,它可以从属性文件中读取数据并动态创建请求处理程序。

属性文件中字段的格式 - =HTTP_METHOD:STATIC_URL

这应该翻译成类似 -

@RequestMapping(path = <regEx_URL>, method = HTTP_METHOD)
private String handler(){
 // do something
 return "working!";
}

我们能实现这个目标吗?

我的想法是,如果我们可以以某种方式配置 Spring MVC 上下文以在容器启动期间注册这些端点,我们就可以服务 HTTP 请求。

我在网上找不到真正适合我的用例的解决方案。任何建议或解决方案将不胜感激!

spring spring-boot rest spring-mvc spring-restcontroller
3个回答
0
投票

我不明白拥有属性文件和动态创建方法/控制器的实际需要是什么。

据我了解你的问题。,
您将在

/users/1/ID

收到请求 每当您收到此类信息时,请转发至
/users/ID

为什么不转发?

控制器类:

@RequestMapping("/v1/customers")

@PostMapping("/{id}/ID")
public Response..<> customers(@PathVariable(value="id"..) String id){
   customersMain(id)
}

@PostMapping("/ID")
public Response..<> customersMain(@RequestParam(value = "theId") String theId){
   //do changes
}

所以,电话总是打来

/v1/customers/1/ID

在内部,它映射到
/v1/customers/ID


0
投票

通过以下方法解决了问题 - 使用 JavaPoet 创建 Sping Beans

我可以读取和处理生成器 java 类中的属性文件,并在编译期间动态创建我的其余控制器。


0
投票

我最终以与此处描述的方式类似的方式进行,但我将尝试展示一个示例。

我正在一个大型项目中工作,有超过 1000 个使用 spring 注释映射的端点。 我想看看是否可以使用 application.yml 或 application.properties(或另一个 propertySource)提供的数据填充实现 ConfigurationProperties 的自定义类,然后使用该映射在运行时定义我的端点。

花了一些时间,但这“确实”有效。我不能说这是我写过的最漂亮的代码,而且我们最终也没有使用它。

ExampleService.java 只是一个 @Service 风格的 bean,其业务逻辑用于从某个地方检索数据,例如数据库、rabbitmq、文件或其他某种存储方式。在下面的示例中,它只是有一个名为“name”的字符串变量来解释 @ConfigurationProperties 如何工作。

定义自定义配置属性:

@ConfigurationProperties(prefix = ExampleConfigurationProperties.PREFIX)
@ConstructorBinding
public class ExampleConfigurationProperties {
    /**
     * Properties prefix.
     */
    public static final String PREFIX = "custom.endpoints";

    private final Set<ExampleService> exampleService;

    public ExampleConfigurationProperties(@Nullable final Set<ExampleService> services) {
        this.exampleService = services;
    }

    public Set<ExampleService> getExampleServices() {
        return exampleService;
    }
}

在您的 application.yml 或属性(属性类似但写法不同)中,您将需要类似以下内容:

custom:
  endpoints:
    services[0]:
      name: SomeName
      # also some other properties in the ExampleService to map
    services[1]:
      name: SomeOtherName
      # also some other properties in the ExampleService to map

配置类,构造函数中重要的部分是

@预选赛

,在此应用程序中定义了 4 个 RequestMappingHandlerMapping 类型的 bean,我需要指定要将端点连接到的 bean。我最终连接了一个列表并在调试模式下找到它们以识别我想要的列表。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ExampleConfigurationProperties.class)
public class ExampleSpringConfig implements ApplicationContextAware {
    private final ExampleConfigurationProperties properties;
    public ExampleSpringConfig (
        final ExampleConfigurationProperties properties,
        @Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping handlerMapping) {
        this.properties = Objects.requireNonNull(properties);
    }
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        //there is probably a better way of getting this beanFactory but I have no reason to further research this as we did not end up going this route.
        ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
        for (ExampleService svc : properties.getExampleService) {
            // good spot to "wire" in any required dependencies/beans for this "Properties" mapped list of objects
            // svc.setRandomInternalProperty(<someValue>);
            
            // give your new "Properties" initiated object a unique name and make it a bean in the current context
            beanFactory.registerSingleton(svc.getClass().getSimpleName() + "_" + svc.getName(), svc);
            
            // this will map the controllers methods to the "Properties" mapped name paramenter appended to "Endpoint"
            ExampleControllerRest controller =
                ExampleControllerRest.createController(handlerMapping, "/" + svc.getName() + "Endpoint", svc);

            // once the controller has all of its methods I want exposed mapped, register the controller as a Spring Bean for use elsewhere
            beanFactory.registerSingleton(controller.getClass().getSimpleName() + "_" + svc.getName(), controller);
        }
    }
}

然后是Controller类:

public class ExampleController {
    private final ExampleService exampleService;

    public ExampleController(final ExampleService exampleService) {
        this.exampleService = exampleService;
    }

    public static ExampleController createController(RequestMappingHandlerMapping handlerMapping, String path, ExampleService exampleService) {
        try (DynamicType.Unloaded<ExampleController> temp = new ByteBuddy()
                .subclass(ExampleController.class)
                .annotateType(AnnotationDescription.Builder
                        .ofType(RequestMapping.class)
                        .defineArray("value", new String[]{path})
                        .build())
                .annotateType(AnnotationDescription.Builder
                        .ofType(RestController.class)
                        .build())
                .make();
        ) {
            ExampleController controller = temp.load(ExampleController.class.getClassLoader())
                    .getLoaded()
                    .getConstructor(ExampleService.class)
                    .newInstance(baseUnitAssignmentsDTOService);

            RequestMappingInfo.BuilderConfiguration builderConfiguration = new RequestMappingInfo.BuilderConfiguration();
            // this was important to get a response back that replicated what Spring autowiring does with custom 
            // response types, otherwise an error is thrown about an empty response body
            // if you are going to use ResponseEntity for response object type wrapping, this might not be needed
            builderConfiguration.setContentNegotiationManager(handlerMapping.getContentNegotiationManager());

            // the pathMatcher was added to have these endpoints look exactly the same as the ones that Spring autowires, I am not sure if this is needed
            AntPathMatcher pathMatcher = (AntPathMatcher) handlerMapping.getPathMatcher();
            pathMatcher.match(path, path);
            builderConfiguration.setPathMatcher(pathMatcher);

            // handler endpoint mappings, these are all of the endpoints I want defined for each element in the custom "Properties" set of services
            handlerMapping.registerMapping(RequestMappingInfo
                    .paths(path + "/getAll")
                    .methods(RequestMethod.GET)
                    .options(builderConfiguration)
                    .produces(MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE)
                    .build(), controller, controller.getClass().getMethod("getAll"));
            // I have redacted a bunch of endpoints but the essential part is to define all endpoints similar to the "getAll" above

            handlerMapping.registerMapping(RequestMappingInfo
                    .paths(path + "/insert")
                    .methods(RequestMethod.POST)
                    .options(builderConfiguration)
                    .consumes(MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE)
                    .produces(MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE)
                    .build(), controller, controller.getClass().getMethod("insert", PickListOptionDTO.class)); 
                    // just remember to define all method body param types for reflection to find the corret method


            return controller;
        } catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException |
                 NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }


    public List<PickListOptionDTO> getAll() {
        return getService().getAllUsers();
    }

    public PickListOptionDTO insert(@RequestBody PickListOptionDTO dto) {
        return getService().insertUnit(dto);
    }

    public ExampleService getService() {
        return exampleService;
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.