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

问题描述

假设我的父类具有以下控制器:

@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?

这就是我一直在努力的问题,无论在StackOverflow还是其他地方,我都找不到与Spring相关的任何东西。

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

我正在处理的项目基于Spring Boot 2.1

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

解决方法

由于看起来您想保留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"})

,

我终于找到了一种以javax.servlet.Filterorg.springframework.web.server.WebFilter实现的方式来实现。

实际上,我引入了Adapter模式以同时转换两者:

  • org.springframework.http.server.ServletServerHttpResponse(非反应性)和
  • org.springframework.http.server.reactive.ServerHttpResponse(活动)

因为与Spring的HTTP请求的包装器共享org.springframework.http.HttpRequest(让我同时访问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();
            }
        };
    }

}

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...