Why Spring Cloud Gateway Rejects MVC: DeferredImportSelector & Conditional Auto-Configuration
This article explains why Spring Cloud Gateway cannot coexist with Spring MVC due to fundamental model differences, and details how Spring Boot's DeferredImportSelector and conditional annotations enable intelligent auto-configuration by evaluating classpath, beans, and environment at runtime.
Gateway + MVC: The Classic Conflict
Adding spring-boot-starter-web to a Spring Cloud Gateway project triggers a startup failure:
Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.This error comes from Gateway's own GatewayClassPathWarningAutoConfiguration, not Spring Boot.
How Gateway Detects MVC
The configuration class uses two conditional inner classes: SpringMvcFoundOnClasspathConfiguration with
@ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")— activates when DispatcherServlet is present, logging a warning (or throwing an exception in newer versions). WebfluxMissingFromClasspathConfiguration with
@ConditionalOnMissingClass("org.springframework.web.reactive.DispatcherHandler")— activates when WebFlux is absent, prompting to add spring-boot-starter-webflux.
Thus Gateway "detects" MVC by checking for specific classes on the classpath via conditional annotations.
Why Gateway and MVC Cannot Coexist
Fundamental Model Differences
The conflict stems from three layers:
Web Server Conflict: MVC defaults to Tomcat (Servlet container); WebFlux uses Netty (non-blocking). Both produce ServletWebServerFactory and ReactiveWebServerFactory beans, causing a bean clash.
Request Dispatcher Conflict: MVC uses DispatcherServlet; WebFlux uses DispatcherHandler. Two dispatchers cannot share the request pipeline.
Programming Model Conflict: MVC controllers return synchronous values on Servlet threads; WebFlux controllers return Mono / Flux on Netty event loops. Mixing blocking calls in Gateway filters blocks Netty threads, causing performance collapse or deadlock.
In short: Gateway is built for high-performance reactive processing; MVC is designed for traditional blocking Servlets. Their underlying models are incompatible.
Correct Way to Add Endpoints in Gateway
Use WebFlux-style endpoints — no extra dependency needed because Gateway already pulls in spring-boot-starter-webflux:
@RestController
public class HealthController {
@GetMapping("/health")
public Mono<String> health() {
return Mono.just("ok");
}
}Or use functional routing with RouterFunction.
DeferredImportSelector: The Engine of "Cook According to the Dishes"
Entry Point: @EnableAutoConfiguration
@SpringBootApplicationincludes @EnableAutoConfiguration, which declares @Import(AutoConfigurationImportSelector.class). AutoConfigurationImportSelector implements DeferredImportSelector.
ImportSelector vs DeferredImportSelector
ImportSelector: executes immediately during configuration class parsing — too early to see user-defined beans. DeferredImportSelector: executes after all @Configuration classes and @ComponentScan beans are processed . This delay ensures conditional annotations can accurately evaluate the final classpath, bean registry, and environment.
Execution Flow in ConfigurationClassParser
public void parse(Set<BeanDefinitionHolder> configCandidates) {
// 1. Parse all user configuration classes
for (BeanDefinitionHolder holder : configCandidates) {
parse(holder.getBeanDefinition().getMetadata(), holder.getBeanName());
}
// 2. Process regular ImportSelectors (immediate)
// ...
// 3. Process DeferredImportSelectors (delayed)
this.deferredImportSelectorHandler.process();
}The handler sorts and groups deferred selectors, then calls processGroupImports() which invokes each group's process() and selectImports().
AutoConfigurationImportSelector's Group Mechanism
AutoConfigurationImportSelectordefines an inner AutoConfigurationGroup implementing DeferredImportSelector.Group:
private static class AutoConfigurationGroup implements DeferredImportSelector.Group {
private final Map<String, AnnotationMetadata> entries = new LinkedHashMap<>();
private final List<AutoConfigurationEntry> autoConfigurationEntries = new ArrayList<>();
@Override
public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
AutoConfigurationEntry entry = ((AutoConfigurationImportSelector) selector)
.getAutoConfigurationEntry(metadata);
this.autoConfigurationEntries.add(entry);
for (String importClassName : entry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, metadata);
}
}
@Override
public Iterable<Entry> selectImports() {
List<String> configurations = new ArrayList<>(this.entries.keySet());
configurations.sort(AUTO_CONFIGURATION_ORDER_COMPARATOR); // @AutoConfigureBefore/After/Order
List<Entry> result = new ArrayList<>();
for (String configuration : configurations) {
result.add(new Entry(configuration, this.entries.get(configuration)));
}
return result;
}
}Two-phase process:
process() : Loads all candidate auto-configuration classes from
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, applies conditional filtering ( @ConditionalOnClass, @ConditionalOnMissingBean, etc.), and stores the filtered set.
selectImports() : Sorts the surviving configurations via @AutoConfigureBefore, @AutoConfigureAfter, @AutoConfigureOrder, and returns the final import list.
Core Filtering: getAutoConfigurationEntry()
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata metadata) {
if (!isEnabled(metadata)) return EMPTY_ENTRY;
AnnotationAttributes attributes = getAttributes(metadata);
List<String> configurations = getCandidateConfigurations(metadata, attributes); // from .imports files
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(metadata, attributes);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata); // CONDITION EVALUATION
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}The filter() step evaluates every conditional annotation on each auto-configuration class: @ConditionalOnClass — class present on classpath @ConditionalOnMissingClass — class absent @ConditionalOnBean / @ConditionalOnMissingBean — bean present/absent in container @ConditionalOnWebApplication — Servlet, Reactive, or non-web @ConditionalOnProperty — configuration property value
Only when all conditions match does the configuration survive.
Back to Gateway: Conditional Assembly in Action
Gateway Auto-Configuration Loading
Gateway registers its auto-configurations in
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration
org.springframework.cloud.gateway.config.GatewayAutoConfiguration
org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration
org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration
...GatewayClassPathWarningAutoConfiguration Conditions
As shown earlier, its two inner classes use @ConditionalOnClass(DispatcherServlet) and @ConditionalOnMissingClass(DispatcherHandler) to act as sentinels.
Why Spring Boot's MVC Auto-Configuration Doesn't Activate
WebMvcAutoConfigurationcarries
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET). When both MVC and WebFlux are present, Spring Boot detects a Reactive environment (Gateway forces spring.main.web-application-type=reactive), so the SERVLET condition fails and WebMvcAutoConfiguration is filtered out. However, DispatcherServlet class remains on the classpath, so Gateway's sentinel detects it and aborts startup.
The error "Spring MVC found on classpath" appears because MVC's auto-configuration is suppressed, but its classes are still present — Gateway's sentinel sees them and refuses to start.
Complete Conditional Assembly Flow for Gateway + MVC
Spring Boot Startup
│
▼
@EnableAutoConfiguration
│
▼
AutoConfigurationImportSelector (DeferredImportSelector)
│
▼
Deferred execution: wait for all user config
│
▼
Load all auto-configurations from .imports files
│
├── GatewayClassPathWarningAutoConfiguration
│ ├── SpringMvcFoundOnClasspathConfiguration
│ │ └── @ConditionalOnClass(DispatcherServlet) → ✅ MVC present → ACTIVATES → ERROR
│ └── WebfluxMissingFromClasspathConfiguration
│ └── @ConditionalOnMissingClass(DispatcherHandler) → ❌ WebFlux present → SKIPPED
│
├── WebMvcAutoConfiguration
│ └── @ConditionalOnWebApplication(SERVLET) → ❌ Reactive env → SKIPPED
│
├── GatewayAutoConfiguration
│ └── @ConditionalOnProperty + @ConditionalOnClass → ✅ ACTIVATES
│
└── ... other auto-configurations
│
▼
Filtered configurations imported
│
▼
Gateway sentinel detects MVC → startup failsThis is "cook according to the dishes": inspect classpath dependencies, container beans, environment type, and configuration properties to decide which auto-configurations apply.
Practice: Building a "Cook According to the Dishes" Starter
Requirement: Automatic Logging Framework Switch
A logging starter that chooses Logback when present, otherwise falls back to JDK Logging.
Interface and Implementations
public interface MyLogger { void log(String message); }
public class LogbackLogger implements MyLogger {
private final org.slf4j.Logger logger = LoggerFactory.getLogger("my-logger");
public void log(String message) { logger.info("[Logback] " + message); }
}
public class JdkLogger implements MyLogger {
private final java.util.logging.Logger logger = java.util.logging.Logger.getLogger("my-logger");
public void log(String message) { logger.info("[JDK] " + message); }
}Conditional Auto-Configuration Classes
@AutoConfiguration
@ConditionalOnClass(name = "ch.qos.logback.classic.LoggerContext")
@ConditionalOnMissingBean(MyLogger.class)
public class LogbackAutoConfiguration {
@Bean
public MyLogger myLogger() { return new LogbackLogger(); }
}
@AutoConfiguration
@ConditionalOnMissingClass("ch.qos.logback.classic.LoggerContext")
@ConditionalOnMissingBean(MyLogger.class)
@AutoConfigureAfter(LogbackAutoConfiguration.class)
public class JdkLoggerAutoConfiguration {
@Bean
public MyLogger myLogger() { return new JdkLogger(); }
}Registration
In
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.example.logging.LogbackAutoConfiguration
com.example.logging.JdkLoggerAutoConfigurationBehavior
Logback on classpath → LogbackLogger injected.
No Logback → JdkLogger injected.
User defines own MyLogger bean → user's bean wins (via @ConditionalOnMissingBean).
This mirrors exactly how Gateway detects MVC.
Summary
Gateway vs MVC Root Cause: Gateway uses WebFlux+Reactor+Netty (non-blocking reactive); MVC uses Servlet+Tomcat (blocking). Web server, dispatcher, and programming models all conflict.
Gateway's MVC Detection: GatewayClassPathWarningAutoConfiguration uses @ConditionalOnClass(DispatcherServlet) to spot MVC classes and abort.
DeferredImportSelector's Role: AutoConfigurationImportSelector implements it, delaying auto-configuration import until all user configuration is parsed. This ensures conditional annotations ( @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnWebApplication) evaluate accurately.
Full Auto-Configuration Flow: @EnableAutoConfiguration → @Import(AutoConfigurationImportSelector) → deferred execution → load candidates from .imports → condition filtering → sorting via @AutoConfigureBefore/After → import final set.
Gateway Best Practices: Never add spring-boot-starter-web; write endpoints with WebFlux ( Mono / Flux or RouterFunction); exclude transitive spring-web; avoid blocking calls in filters. DeferredImportSelector embodies Spring Boot's "convention over configuration" philosophy: the framework doesn't decide for the user; it intelligently provides defaults based on the environment, with user configuration always taking precedence. Mastering this mechanism turns guesswork into precise diagnostics for auto-configuration failures, bean conflicts, and dependency clashes.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
