1. 引言

在Spring Boot应用中,每个控制器都可以有自己的URL映射。这使得单个应用能够在一个或多处提供Web端点。例如,我们可以将API端点分组为逻辑分组,如内部和外部。

然而,有时我们可能希望所有端点都使用一个共同的前缀。在这篇教程中,我们将探讨为Spring Boot控制器使用共同前缀的不同方法。

2. Servlet上下文

在Spring应用中处理网络请求的主要组件是*DispatcherServlet*。通过自定义这个组件,我们可以对请求路由有相当大的控制权。

让我们看看两种不同的方式来定制DispatcherServlet,使其使我们应用的所有端点都可通过一个共享URL前缀访问。

2.1. Spring Bean

第一种方法是引入一个新的Spring Bean:

@Configuration
public class DispatcherServletCustomConfiguration {

    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    @Bean
    public ServletRegistrationBean dispatcherServletRegistration() {
        ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet(), "/api/");
        registration.setName(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME);
        return registration;
    }
}

在这里,我们创建了一个ServletRegistrationBean,它包装了DispatcherServlet的bean。请注意,我们提供了明确的基URL/api/。这意味着所有端点必须通过这个基URL前缀访问。

2.2. 应用属性

我们也可以仅通过使用应用属性达到同样的效果。在Spring Boot 2.0.0版本之后,我们在application.properties文件中添加以下内容:

server.servlet.contextPath=/api

在那个版本之前,属性名称稍有不同:

server.contextPath=/api

这种方法的一个优点是它只使用标准的Spring属性。这意味着我们可以使用标准机制,如配置文件或外部属性绑定,轻松更改或覆盖我们的共享前缀。

2.3. 优缺点

这两种方法的优点也是其主要缺点:它们影响应用中的每一个端点。

对于某些应用,这可能是完全合适的。然而,有些应用可能需要使用标准端点映射与第三方服务交互,比如OAuth交换。在这种情况下,全局解决方案可能并不适用。

3. 注解

另一种为Spring应用中的所有控制器添加前缀的方法是使用注解。下面我们将探讨两种不同的方法。

3.1. SpEL(Spring Expression Language)

第一种方法是使用Spring Expression Language(SpEL)与标准的@RequestMapping注解。通过这种方法,我们只需在每个想要添加前缀的控制器上添加一个属性:

@Controller
@RequestMapping(path = "${apiPrefix}/users")
public class UserController {

} 

然后,我们在application.properties中指定属性值:

apiPrefix=/api

3.2. 自定义注解

另一种实现此目的的方式是创建我们自己的注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@RequestMapping("/api/")
public @interface ApiPrefixController {
    @AliasFor(annotation = Component.class)
    String value() default "";
}

接着,我们只需要将注解应用到我们想要添加前缀的每个控制器上:

@Controller
@ApiPrefixController
public class SomeController {
    @RequestMapping("/users")
    @ReponseBody
    public String getAll(){
        // ...
    }
}

3.3. 优缺点

这两种方法解决了先前方法的主要问题:它们都提供了对哪些控制器应用前缀的细粒度控制。我们可以只将注解应用于特定控制器,而不是影响应用中的所有端点。

4. 服务器端转发

我们将探讨的最后一种方法是使用服务器端转发与重定向不同,转发不涉及向客户端发送响应。这意味着我们的应用可以在不影响客户端的情况下在端点之间传递请求。

首先,我们编写一个简单的控制器,包含两个端点:

@Controller
class EndpointController {
    @GetMapping("/endpoint1")
    @ResponseBody
    public String endpoint1() {
        return "Hello from endpoint 1";
    }

    @GetMapping("/endpoint2")
    @ResponseBody
    public String endpoint2() {
        return "Hello from endpoint 2";
    }
}

接下来,我们基于我们想要的前缀创建一个新的控制器:

@Controller
@RequestMapping("/api/endpoint")
public class ApiPrefixController {

    @GetMapping
    public ModelAndView route(ModelMap model) {
        if(new Random().nextBoolean()) {
            return new ModelAndView("forward:/endpoint1", model);
        } 
        else {
            return new ModelAndView("forward:/endpoint2", model);
        }
    }
}

这个控制器只有一个端点,作为路由器。在这种情况下,它基本上是抛硬币决定将原始请求转发到我们的其他两个端点之一。

我们可以通过发送几次连续请求来验证它是否工作:

> curl http://localhost:8080/api/endpoint
Hello from endpoint 2
> curl http://localhost:8080/api/endpoint
Hello from endpoint 1
> curl http://localhost:8080/api/endpoint
Hello from endpoint 1
> curl http://localhost:8080/api/endpoint
Hello from endpoint 2
> curl http://localhost:8080/api/endpoint
Hello from endpoint 2

这种方法的主要优点是它非常强大。我们可以根据URL路径、HTTP方法、HTTP头等任何逻辑来确定如何转发请求。

5. 总结

在这篇文章中,我们学习了为Spring应用中的每个控制器应用共享前缀的几种方法。正如大多数决策一样,每种方法都有其优缺点,实施前应仔细考虑。

一如既往,本教程的代码示例可在GitHub上找到。