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

How to rewrite URLs with Spring (Boot) via REST Controllers?

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

@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 但通过返回 301 HTTP 状态代码和新 URI

使其保持“可用” ]

例如客户请求:

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?

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

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

我正在进行的项目基于 Spring Boot 2.1

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

因为看起来您想要保留 301 但又想得到它 return 响应,您可以选择将 RootController 连接到 LegacyRootController

这样您就可以重用 RootController 中的逻辑,但 return 不同的响应代码并在 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 标准响应。

如果您想添加位置 header,您可以让 LegacyRootController return 一个 ResponseEntity 提供 body 代码和 header 个值。

@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"})

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

其实我引入Adapter模式就是为了改造两者:

  • org.springframework.http.server.ServletServerHttpResponse (non-reactive) 和
  • 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();
            }
        };
    }

}