一文詳解SpringBoot如何同時(shí)監(jiān)聽多個(gè)端口
前言
在日常開發(fā)中,我們通常構(gòu)建的 Spring Boot 應(yīng)用都是"單面"的——一個(gè)端口,一套服務(wù)邏輯。但在某些實(shí)際場(chǎng)景中,我們可能需要一個(gè)應(yīng)用能夠"一心二用":同時(shí)提供兩套完全不同的服務(wù),分別在不同的端口上運(yùn)行。
比如:
- 一個(gè)端口面向外部用戶,提供 API 服務(wù)
- 另一個(gè)端口面向內(nèi)部管理,提供監(jiān)控和運(yùn)維功能
- 或者在一個(gè)應(yīng)用中同時(shí)集成管理后臺(tái)和用戶前臺(tái)
場(chǎng)景示例
假設(shè)我們要開發(fā)一個(gè)電商平臺(tái),需要同時(shí)滿足:
用戶端服務(wù)(端口8082)
- 商品瀏覽
- 購(gòu)物車管理
- 訂單處理
管理端服務(wù)(端口8083)
- 商品管理
- 訂單管理
- 數(shù)據(jù)統(tǒng)計(jì)
這兩套服務(wù)功能完全不同,但需要部署在同一個(gè)應(yīng)用中。
技術(shù)實(shí)現(xiàn)方案
方案一:多 Tomcat Connector 配置
最直接的方式是配置多個(gè) Tomcat Connector。
1. 創(chuàng)建基礎(chǔ)項(xiàng)目結(jié)構(gòu)
// 主應(yīng)用類
@SpringBootApplication
public class DualPortApplication {
public static void main(String[] args) {
SpringApplication.run(DualPortApplication.class, args);
}
}
2. 配置雙端口
@Configuration
public class DualPortConfiguration {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
// 添加第一個(gè)連接器(用戶端)
factory.addAdditionalTomcatConnectors(createUserPortConnector());
// 添加第二個(gè)連接器(管理端)
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. 路由分離策略
現(xiàn)在我們需要為不同端口提供不同的路由處理:
@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) {
// 用戶端請(qǐng)求
httpRequest.setAttribute("serviceType", "USER");
} else if (port == 8083) {
// 管理端請(qǐng)求
httpRequest.setAttribute("serviceType", "ADMIN");
}
chain.doFilter(request, response);
}
}
4. 創(chuàng)建分離的 Controller
// 用戶端 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";
}
}
方案二:基于路徑前綴的更優(yōu)雅方案
上述方案雖然可行,但在實(shí)際使用中可能會(huì)有一些問題。讓我們采用更優(yōu)雅的方案。
1. 自定義 Web MVC 配置
@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. 使用注解標(biāo)記 Controller
@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 "創(chuàng)建商品";
}
@PutMapping("/{id}")
public String updateProduct(@PathVariable String id) {
return "更新商品: " + id;
}
}
高級(jí)特性實(shí)現(xiàn)
1. 端口感知的攔截器
@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) {
// 用戶端請(qǐng)求驗(yàn)證邏輯
String userAgent = request.getHeader("User-Agent");
if (userAgent == null) {
throw new SecurityException("Invalid user request");
}
}
private void validateAdminRequest(HttpServletRequest request) {
// 管理端請(qǐng)求驗(yàn)證邏輯
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new SecurityException("Admin authentication required");
}
}
}
2. 端口特定的異常處理
@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("用戶服務(wù)異常: " + e.getMessage());
} else if (port == 8083) {
error.setCode("ADMIN_ERROR_" + e.hashCode());
error.setMessage("管理服務(wù)異常: " + e.getMessage());
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(error);
}
}
3. 動(dòng)態(tài)端口配置
@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;
}
}
監(jiān)控和日志
1. 分端口日志記錄
@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("用戶端請(qǐng)求: {} {}", method, uri);
} else if (port == 8083) {
adminLogger.info("管理端請(qǐng)求: {} {}", method, uri);
}
}
}
2. 端口特定的健康檢查
@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;
}
}
安全考慮
端口訪問控制
@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();
}
}
總結(jié)
構(gòu)建"雙面" Spring Boot 應(yīng)用是一個(gè)有趣且實(shí)用的技術(shù)挑戰(zhàn)。通過本文介紹的多種實(shí)現(xiàn)方案,我們可以根據(jù)實(shí)際需求選擇最適合的方式:
多 Connector 方案:適合簡(jiǎn)單場(chǎng)景,實(shí)現(xiàn)直接
路徑前綴方案:適合需要清晰 API 結(jié)構(gòu)的場(chǎng)景
在某些特定場(chǎng)景下確實(shí)能夠簡(jiǎn)化系統(tǒng)架構(gòu),降低運(yùn)維成本。但同時(shí)也要注意避免過度復(fù)雜化,確保系統(tǒng)的可維護(hù)性和可擴(kuò)展性。
到此這篇關(guān)于一文詳解SpringBoot如何同時(shí)監(jiān)聽多個(gè)端口的文章就介紹到這了,更多相關(guān)SpringBoot監(jiān)聽多個(gè)端口內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot使用Mybatis-Plus中分頁(yè)插件PaginationInterceptor詳解
文章介紹SpringBoot高版本中使用MyBatisPlusInterceptor替代舊分頁(yè)插件,需配置多個(gè)InnerInterceptor功能模塊(如分頁(yè)、多租戶、動(dòng)態(tài)表名等),并強(qiáng)調(diào)插件順序和mapper.xml中SQL語句不能以分號(hào)結(jié)尾,以避免分頁(yè)語法錯(cuò)誤2025-07-07
SpringBoot文件上傳大小設(shè)置方式(yml中配置)
這篇文章主要介紹了SpringBoot文件上傳大小設(shè)置方式(yml中配置),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
Java為何需要平衡方法調(diào)用與內(nèi)聯(lián)
這篇文章主要介紹了Java為何需要平衡方法調(diào)用與內(nèi)聯(lián),幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2021-01-01
list集合去除重復(fù)對(duì)象的實(shí)現(xiàn)
下面小編就為大家?guī)硪黄猯ist集合去除重復(fù)對(duì)象的實(shí)現(xiàn)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-01-01
關(guān)于HashSet與HashMap的區(qū)別及說明
這篇文章主要介紹了關(guān)于HashSet與HashMap的區(qū)別及說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07

