前言
在日常开发中,我们通常构建的 Spring Boot 应用都是"单面"的------一个端口,一套服务逻辑。但在某些实际场景中,我们可能需要一个应用能够"一心二用":同时提供两套完全不同的服务,分别在不同的端口上运行。
比如:
- 一个端口面向外部用户,提供 API 服务
- 另一个端口面向内部管理,提供监控和运维功能
- 或者在一个应用中同时集成管理后台和用户前台
场景示例
假设我们要开发一个电商平台,需要同时满足:
用户端服务(端口8082)
- 商品浏览
- 购物车管理
- 订单处理
管理端服务(端口8083)
- 商品管理
- 订单管理
- 数据统计
这两套服务功能完全不同,但需要部署在同一个应用中。
技术实现方案
方案一:多 Tomcat Connector 配置
最直接的方式是配置多个 Tomcat Connector。
1. 创建基础项目结构
java
// 主应用类
@SpringBootApplication
public class DualPortApplication {
public static void main(String[] args) {
SpringApplication.run(DualPortApplication.class, args);
}
}
2. 配置双端口
java
@Configuration
public class DualPortConfiguration {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
// 添加第一个连接器(用户端)
factory.addAdditionalTomcatConnectors(createUserPortConnector());
// 添加第二个连接器(管理端)
factory.addAdditionalTomcatConnectors(createAdminPortConnector());
return factory;
}
private Connector createUserPortConnector() {
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setPort(8080);
connector.setProperty("connectionTimeout", "20000");
return connector;
}
private Connector createAdminPortConnector() {
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setPort(8081);
connector.setProperty("connectionTimeout", "20000");
return connector;
}
}
3. 路由分离策略
现在我们需要为不同端口提供不同的路由处理:
java
@Component
public class PortBasedFilter implements Filter {
private static final String USER_PORT_HEADER = "X-User-Port";
private static final String ADMIN_PORT_HEADER = "X-Admin-Port";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
int port = httpRequest.getLocalPort();
if (port == 8082) {
// 用户端请求
httpRequest.setAttribute("serviceType", "USER");
} else if (port == 8083) {
// 管理端请求
httpRequest.setAttribute("serviceType", "ADMIN");
}
chain.doFilter(request, response);
}
}
4. 创建分离的 Controller
java
// 用户端 Controller
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/products")
public String getProducts() {
return "User Products API";
}
@PostMapping("/cart")
public String addToCart() {
return "Add to cart";
}
}
// 管理端 Controller
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/products")
public String manageProducts() {
return "Admin Products Management";
}
@GetMapping("/statistics")
public String getStatistics() {
return "Admin Statistics";
}
}
方案二:基于路径前缀的更优雅方案
上述方案虽然可行,但在实际使用中可能会有一些问题。让我们采用更优雅的方案。
1. 自定义 Web MVC 配置
java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
// 为用户端配置前缀
configurer.addPathPrefix("/user", cls -> cls.isAnnotationPresent(UserApi.class));
// 为管理端配置前缀
configurer.addPathPrefix("/admin", cls -> cls.isAnnotationPresent(AdminApi.class));
}
}
// 定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserApi {}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminApi {}
2. 使用注解标记 Controller
java
@RestController
@RequestMapping("/products")
@UserApi
public class UserProductController {
@GetMapping
public String getProducts() {
return "用户端商品列表";
}
@GetMapping("/{id}")
public String getProduct(@PathVariable String id) {
return "商品详情: " + id;
}
}
@RestController
@RequestMapping("/products")
@AdminApi
public class AdminProductController {
@GetMapping
public String getAllProducts() {
return "管理端商品管理列表";
}
@PostMapping
public String createProduct() {
return "创建商品";
}
@PutMapping("/{id}")
public String updateProduct(@PathVariable String id) {
return "更新商品: " + id;
}
}
高级特性实现
1. 端口感知的拦截器
java
@Component
public class PortAwareInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
int port = request.getLocalPort();
if (port == 8082) {
// 用户端逻辑
validateUserRequest(request);
} else if (port == 8083) {
// 管理端逻辑
validateAdminRequest(request);
}
return true;
}
private void validateUserRequest(HttpServletRequest request) {
// 用户端请求验证逻辑
String userAgent = request.getHeader("User-Agent");
if (userAgent == null) {
throw new SecurityException("Invalid user request");
}
}
private void validateAdminRequest(HttpServletRequest request) {
// 管理端请求验证逻辑
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new SecurityException("Admin authentication required");
}
}
}
2. 端口特定的异常处理
java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(
Exception e, HttpServletRequest request) {
int port = request.getLocalPort();
ErrorResponse error = new ErrorResponse();
if (port == 8082) {
error.setCode("USER_ERROR_" + e.hashCode());
error.setMessage("用户服务异常: " + e.getMessage());
} else if (port == 8083) {
error.setCode("ADMIN_ERROR_" + e.hashCode());
error.setMessage("管理服务异常: " + e.getMessage());
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(error);
}
}
3. 动态端口配置
java
@Configuration
@ConfigurationProperties(prefix = "dual.port")
@Data
public class DualPortProperties {
private int userPort = 8082;
private int adminPort = 8083;
@Bean
public ServletWebServerFactory servletContainer(DualPortProperties properties) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addAdditionalTomcatConnectors(
createConnector("user", properties.getUserPort()));
factory.addAdditionalTomcatConnectors(
createConnector("admin", properties.getAdminPort()));
return factory;
}
private Connector createConnector(String name, int port) {
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setPort(port);
connector.setName(name + "-connector");
return connector;
}
}
监控和日志
1. 分端口日志记录
java
@Configuration
public class LoggingConfiguration {
@Bean
public Logger userLogger() {
return LoggerFactory.getLogger("USER-PORT");
}
@Bean
public Logger adminLogger() {
return LoggerFactory.getLogger("ADMIN-PORT");
}
}
@Component
public class PortAwareLogger {
private final Logger userLogger;
private final Logger adminLogger;
public PortAwareLogger(Logger userLogger, Logger adminLogger) {
this.userLogger = userLogger;
this.adminLogger = adminLogger;
}
public void logRequest(HttpServletRequest request) {
int port = request.getLocalPort();
String uri = request.getRequestURI();
String method = request.getMethod();
if (port == 8082) {
userLogger.info("用户端请求: {} {}", method, uri);
} else if (port == 8083) {
adminLogger.info("管理端请求: {} {}", method, uri);
}
}
}
2. 端口特定的健康检查
java
@Component
public class DualPortHealthIndicator implements HealthIndicator {
@Override
public Health health() {
return Health.up()
.withDetail("user-port", 8082)
.withDetail("admin-port", 8083)
.withDetail("status", "Both ports are active")
.build();
}
}
@RestController
@RequestMapping("/health")
public class HealthController {
@GetMapping("/user")
public Map<String, Object> userHealth() {
Map<String, Object> health = new HashMap<>();
health.put("port", 8082);
health.put("status", "UP");
health.put("service", "user-api");
return health;
}
@GetMapping("/admin")
public Map<String, Object> adminHealth() {
Map<String, Object> health = new HashMap<>();
health.put("port", 8083);
health.put("status", "UP");
health.put("service", "admin-api");
return health;
}
}
安全考虑
1. 端口访问控制
java
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers(req -> req.getLocalPort() == 8082)
.permitAll()
.requestMatchers(req -> req.getLocalPort() == 8083)
.hasRole("ADMIN")
.anyRequest().denyAll()
)
.formLogin(form -> form
.loginPage("/admin/login")
.permitAll()
);
return http.build();
}
}
总结
构建"双面" Spring Boot 应用是一个有趣且实用的技术挑战。通过本文介绍的多种实现方案,我们可以根据实际需求选择最适合的方式:
多 Connector 方案:适合简单场景,实现直接
路径前缀方案:适合需要清晰 API 结构的场景
在某些特定场景下确实能够简化系统架构,降低运维成本。但同时也要注意避免过度复杂化,确保系统的可维护性和可扩展性。