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