如何通过 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 请求包装器相反(让我访问 URI
和 HttpHeaders
),响应的包装器不共享一个通用接口,所以我不得不模仿一个(这里特意以类似的方式命名,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 调用在数据准备好时传输?