SpringBoot “分身术”:同时监听多个端口

前言

在日常开发中,我们通常构建的 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 结构的场景

在某些特定场景下确实能够简化系统架构,降低运维成本。但同时也要注意避免过度复杂化,确保系统的可维护性和可扩展性。

github.com/yuboon/java...

相关推荐
Victor3563 小时前
Redis(74)Redis分布式锁与ZooKeeper分布式锁有何区别?
后端
Victor3564 小时前
Redis(75)Redis分布式锁的性能如何优化?
后端
JaguarJack4 小时前
PHP 8.5 新特性 闭包可以作为常量表达式了
后端·php
毕业设计制作和分享7 小时前
springboot150基于springboot的贸易行业crm系统
java·vue.js·spring boot·后端·毕业设计·mybatis
你的人类朋友11 小时前
【Node】认识multer库
前端·javascript·后端
lang2015092812 小时前
Spring Boot 官方文档精解:构建与依赖管理
java·spring boot·后端
why技术13 小时前
从18w到1600w播放量,我的一点思考。
java·前端·后端
间彧13 小时前
Redis Cluster vs Sentinel模式区别
后端
间彧14 小时前
🛡️ 构建高可用缓存架构:Redis集群与Caffeine多级缓存实战
后端