如何通过 REST 控制器使用 Spring (Boot) 重写 URL?

Posted

技术标签:

【中文标题】如何通过 REST 控制器使用 Spring (Boot) 重写 URL?【英文标题】:How to rewrite URLs with Spring (Boot) via REST Controllers? 【发布时间】:2020-12-21 17:26:57 【问题描述】:

假设我有以下控制器及其父类:

@RestController
public class BusinessController extends RootController 

    @GetMapping(value = "users", produces = "application/json")
    @ResponseBody
    public String users() 
        return " \"users\": [] "
    

    @GetMapping(value = "companies", produces = "application/json")
    @ResponseBody
    public String companies() 
        return " \"companies\": [] "
    



@RestController
@RequestMapping(path = "api")
public class RootController 


通过调用这样的 URL 来检索数据:

http://app.company.com/api/users
http://app.company.com/api/companies

现在假设我想将 /api 路径重命名为 /rest,但通过在新 URI 旁边返回 301 HTTP 状态代码来保持它“可用”

例如客户要求:

GET /api/users HTTP/1.1
Host: app.company.com

服务器请求:

HTTP/1.1 301 Moved Permanently
Location: http://app.company.com/rest/users

所以我打算在我的父控制器中从"api" 更改为"rest"

@RestController
@RequestMapping(path = "rest")
public class RootController 


然后引入一个“遗留”控制器:

@RestController
@RequestMapping(path = "api")
public class LegacyRootController 


但是现在如何让它“重写”“旧”URI?

这就是我正在努力解决的问题,无论是在 *** 上还是在其他地方,我都找不到任何与 Spring 相关的内容。

我还有很多控制器和很多方法端点,所以我不能手动执行此操作(即通过编辑每个 @RequestMapping/@GetMapping 注释)。

我正在做的项目是基于 Spring Boot 2.1

编辑:我删除了/business 路径,因为实际上继承在“默认情况下”不起作用(请参阅Spring MVC @RequestMapping Inheritance 或Modifying @RequestMappings on startup 之类的问题和答案) - 抱歉。

【问题讨论】:

我已经提供了一个答案,该答案对这个问题采取了直截了当的方法。如果您想在对您的应用程序的每个请求上都有一个毯子 301 和位置标头,请让我知道您是否有扩展 MvcConfigurerAdapter 的配置 实际上,由于该项目基于 Spring Boot,我只有一个使用 @SpringBootApplication 注释的配置类和一堆其他使用传统 Spring 的 @Configuration 注释的配置类,它们提供了一些 @Bean@Component;其中只有 1 个是 public class SecurityConfig extends WebSecurityConfigurerAdapter 来自定义 Spring Security。 【参考方案1】:

我终于找到了实现这一点的方法,既可以作为javax.servlet.Filter 也可以作为org.springframework.web.server.WebFilter 实现。

事实上,我引入了适配器模式是为了转换两者:

org.springframework.http.server.ServletServerHttpResponse(非反应性)和 org.springframework.http.server.reactive.ServerHttpResponse(反应式)

因为与共享 org.springframework.http.HttpRequest 的 Spring 的 HTTP 请求包装器相反(让我访问 URIHttpHeaders),响应的包装器不共享一个通用接口,所以我不得不模仿一个(这里特意以类似的方式命名,HttpResponse)。

@Component
public class RestRedirectWebFilter implements Filter, WebFilter 

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException 
        ServletServerHttpRequest request = new ServletServerHttpRequest((HttpServletRequest) servletRequest);
        ServletServerHttpResponse response = new ServletServerHttpResponse((HttpServletResponse) servletResponse);

        if (actualFilter(request, adapt(response))) 
            chain.doFilter(servletRequest, servletResponse);
        
    

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) 
        if (actualFilter(exchange.getRequest(), adapt(exchange.getResponse()))) 
            return chain.filter(exchange);
         else 
            return Mono.empty();
        
    

    /**
     * Actual filtering.
     * 
     * @param request
     * @param response
     * @return boolean flag specifying if filter chaining should continue.
     */
    private boolean actualFilter(HttpRequest request, HttpResponse response) 
        URI uri = request.getURI();
        String path = uri.getPath();
        if (path.startsWith("/api/")) 
            String newPath = path.replaceFirst("/api/", "/rest/");
            URI location = UriComponentsBuilder.fromUri(uri).replacePath(newPath).build().toUri();
            response.getHeaders().setLocation(location);
            response.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
            response.flush();
            return false;
        
        return true;
    

    interface HttpResponse extends HttpMessage 

        void setStatusCode(HttpStatus status);

        void flush();

    

    private HttpResponse adapt(ServletServerHttpResponse response) 
        return new HttpResponse() 
            public HttpHeaders getHeaders() 
                return response.getHeaders();
            

            public void setStatusCode(HttpStatus status) 
                response.setStatusCode(status);
            

            public void flush() 
                response.close();
            
        ;
    

    private HttpResponse adapt(org.springframework.http.server.reactive.ServerHttpResponse response) 
        return new HttpResponse() 
            public HttpHeaders getHeaders() 
                return response.getHeaders();
            

            public void setStatusCode(HttpStatus status) 
                response.setStatusCode(status);
            

            public void flush() 
                response.setComplete();
            
        ;
    


【讨论】:

【参考方案2】:

由于您似乎想保留 301 并让它返回响应,因此您可以选择将您的 RootController 连接到您的 LegacyRootController

这样,您可以重用 RootController 中的逻辑,但返回不同的响应代码并在您的 LegacyRootController 上提供不同的路径

@RestController
@RequestMapping(path = "api")
public class LegacyRootController 
    
     private final RootController rootController;
     
     public LegacyRootController(RootController rootController)  
         this.rootController = rootController;
     

     @GetMapping(value = "users", produces = "application/json")
     @ResponseStatus(HttpStatus.MOVED_PERMANENTLY) // Respond 301
     @ResponseBody
     public String users() 
        return rootController.users(); // Use rootController to provide appropriate response. 
     

     @GetMapping(value = "companies", produces = "application/json")
     @ResponseStatus(HttpStatus.MOVED_PERMANENTLY)
     @ResponseBody
     public String companies() 
         return rootController.companies();
     

这将允许您提供 /api/users 以提供 301 响应,同时还允许您提供 /rest/users 以您的标准响应。

如果您想添加 Location 标头,可以让您的 LegacyRootController 返回一个 ResponseEntity 以提供正文、代码和标头值。

@GetMapping(value = "users", produces = "application/json")
public ResponseEntity<String> users() 
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setLocation("...");
    return new ResponseEntity<String>(rootController.users(), responseHeaders, HttpStatus.MOVED_PERMANENTLY);

如果你想服务多个不服务不同状态码的端点,你可以简单地提供多个路径

@RequestMapping(path = "api", "rest")

【讨论】:

如果您想在 api 路径上的所有请求上添加一个毯子 301,您可以注册一个拦截器以提供带有 HandlerInterceptor 的响应状态和标头 感谢@shinjw 的回答;我可以理解您的观点,但是与我想要的/可以做的有一些不同:1.我不一定要为“旧” /api 端点提供 JSON 响应,以及 2. RootController 和 LegacyRootController 都是空:他们不持有代码,而例如BusinessController 可以。 此外,您使用拦截器的建议可能很有趣,尽管我需要弄清楚如何检查 URI 是否应该存在;我的意思是我不会用Location: /rest/nonsense 回答301 来请求GET /api/nonsense... ***.com/questions/31082981/… 获得灵感 谢谢,尤其是这个答案或以下答案:***.com/a/55901453/666414

以上是关于如何通过 REST 控制器使用 Spring (Boot) 重写 URL?的主要内容,如果未能解决你的问题,请参考以下文章

通过 REST 控制器使用 Spring Data JPA 和 QueryDsl 的异常

如何将角度 html 表单输入发送到 Spring Boot Rest Web 控制器

如何通过 Spring Boot 的 rest 调用在数据准备好时传输?

如何使用 spring 管理 REST API 版本控制?

如何在 Spring-Data-Rest 中实现细粒度的访问控制?

如何使用 AngularJS 的 $resource 和 spring rest 控制器获取布尔值