How to Build a Dual‑Port Spring Boot App for Separate User and Admin Services

Learn how to create a dual‑port Spring Boot application that serves separate user and admin services on different ports, covering two implementation strategies—multiple Tomcat connectors and path‑prefix routing—plus advanced features like port‑aware interceptors, custom health checks, logging, and security controls.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
How to Build a Dual‑Port Spring Boot App for Separate User and Admin Services

Preface

In typical development a Spring Boot application is single‑port, but some scenarios require a "dual‑port" app that serves two completely different services on separate ports.

Scenario Example

For an e‑commerce platform we may need:

User‑side service (port 8082) providing product browsing, cart management, order processing.

Admin‑side service (port 8083) providing product management, order management, data statistics.

Technical Implementation

Solution 1: Multiple Tomcat Connectors

1. Create basic project

// Main application class
@SpringBootApplication
public class DualPortApplication {
    public static void main(String[] args) {
        SpringApplication.run(DualPortApplication.class, args);
    }
}

2. Configure dual ports

@Configuration
public class DualPortConfiguration {
    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        // Add user port connector
        factory.addAdditionalTomcatConnectors(createUserPortConnector());
        // Add admin port connector
        factory.addAdditionalTomcatConnectors(createAdminPortConnector());
        return factory;
    }

    private Connector createUserPortConnector() {
        Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setPort(8082);
        connector.setProperty("connectionTimeout", "20000");
        return connector;
    }

    private Connector createAdminPortConnector() {
        Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setPort(8083);
        connector.setProperty("connectionTimeout", "20000");
        return connector;
    }
}

3. Routing separation

Use a filter to set a request attribute based on the local port.

@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. Separate controllers

// User side 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";
    }
}

// Admin side 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";
    }
}

Solution 2: Path‑Prefix Approach

This method uses custom WebMvc configuration and annotations to add distinct prefixes for user and admin APIs.

1. Web MVC config

@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));
    }
}

// Marker annotations
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserApi {}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminApi {}

2. Annotated controllers

@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;
    }
}

Advanced Features

1. Port‑aware interceptor

@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. Port‑specific exception handling

@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. Dynamic port configuration

@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;
    }
}

Monitoring and Logging

1. Port‑specific loggers

@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. Port‑specific health checks

@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;
    }
}

Security Considerations

1. Port‑based access control

@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();
    }
}

Conclusion

Building a “dual‑port” Spring Boot application is a practical challenge. The multiple‑connector approach is straightforward for simple cases, while the path‑prefix method offers clearer API separation for more complex requirements. Choose the solution that best fits your architecture, keeping maintainability and scalability in mind.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaMicroservicesBackend DevelopmentSpring BootDual Port
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.