【问题标题】:How to rewrite URLs with Spring (Boot) via REST Controllers?如何通过 REST 控制器使用 Spring (Boot) 重写 URL?
【发布时间】: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?

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

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

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

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

【问题讨论】:

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

标签: spring spring-boot url-rewriting http-status-code-301 request-mapping


【解决方案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...
    • 谢谢,尤其是这个答案或以下答案:stackoverflow.com/a/55901453/666414
    猜你喜欢
    • 2019-11-20
    • 2020-11-13
    • 1970-01-01
    • 2020-01-24
    • 1970-01-01
    • 2018-06-23
    • 2016-09-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多