
1. 为什么SpringMVC项目在IDEA里启动总出问题在IDEA下配置application启动SpringMVC项目这件事听起来简单做起来却经常卡人。我在带新人的时候发现一个规律能顺利跑起来的人往往不是因为技术多强而是因为脑子里有一张清晰的启动链路图。反过来卡住的人九成都是卡在同一类问题上——不知道请求从哪进、配置从哪读、容器由谁提供。IDEA、application、SpringMVC这三个词凑在一起本质上问的是一个工程问题谁负责启动谁负责装配谁负责托管。先把场景说清楚。这里说的application有两种常见含义很多人第一次接触时会混淆。第一种是application.xml或者application.properties这类配置文件负责承载数据源、视图解析器、扫描包路径这些装配信息第二种是Application启动类也就是带main方法的那个入口用来在代码里直接把容器拉起来。第一种是传统SpringMVC的玩法第二种更接近后来Spring Boot的做法。这篇文章把两条路都走一遍因为实际项目里这两种情况都会遇到——老项目是第一种新项目被改造成第二种。这篇文章适合三类人看。第一类是刚学完SpringMVC理论、准备自己搭一个能跑起来的工程的学生或者转行朋友第二类是从Eclipse迁到IDEA、发现原来那套Run方式完全不灵的老开发第三类是接手了遗留项目、需要在本地把war包跑起来排查线上问题的维护人员。不管你基础如何读完至少能自己判断启动不了到底出在哪一层。我个人的感受是SpringMVC在IDEA里跑不起来八成不是Spring本身的问题而是容器和工程的对接没对上。IDEA只是一个编辑器加运行管理的壳它自己不懂Servlet规范真正干活的是Tomcat。你要做的事就是让IDEA知道用哪个Tomcat让Tomcat知道加载哪个工程让工程知道读哪份application配置。这三句话就是整篇文章的主线。2. 先把工程骨架搭对SpringMVC的最小可运行结构2.1 目录结构约定与Maven坐标SpringMVC对目录结构不是随便定的它依赖Servlet规范里的WEB-INF约定。很多人工程跑不起来第一步就错在目录上。标准结构长这样springmvc-demo ├── pom.xml └── src ── main ├── java │ └── com/example │ ├── controller │ │ └── HelloController.java │ └── config ├── resources │ ├── applicationContext.xml │ ├── spring-mvc.xml │ └── application.properties └── webapp ├── WEB-INF │ ├── web.xml │ └── views │ └── hello.jsp ── index.jsp这里必须强调一点webapp目录是web工程的根也就是Tomcat眼里的docBase。WEB-INF下面的东西浏览器直接访问不到这正好用来放不希望被外部直接请求的JSP和配置。我在实际项目里见过有人把web.xml放到src/main/resources下结果Tomcat启动时压根找不到报的一堆错还特别难懂所以目录这件事一开始就要定死。Maven坐标也有讲究。group和artifact随意但packaging必须是war这一条不改后面全白搭。因为jar包没有WEB-INF的概念Servlet容器不认。我一般会这样写groupIdcom.example/groupId artifactIdspringmvc-demo/artifactId version1.0-SNAPSHOT/version packagingwar/packaging很多人用IDEA新建项目时随手选了jar然后一路配到Tomcat Deployment那一步发现Artifact列表里就是找不到能部署的东西回头查半天。这属于典型的起点错了后面全错我建议在这个环节多花十秒钟确认。2.2 pom.xml依赖与打包方式依赖部分的关键点在于javax.servlet-api的scope。它必须写成provided原因很简单Tomcat自己带了Servlet API的实现你如果打成compilewar包里的WEB-INF/lib会多一份Servlet API和容器的实现打架轻则报ClassCastException重则直接启动失败。这是我踩过最多次的坑之一。dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.31/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.31/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies关于版本选择这里插一句实在话。Spring 5.x 用的是javax.servlet包名Spring 6.x 换成了jakarta.servlet。这两者不兼容就像插头和插座换了规格。你如果Tomcat用9Spring就得用5Tomcat用10Spring就得用6同时web.xml里的类名引用也跟着变。版本这件事不是越新越好而是配套才行。我手上有几个老项目就卡在这一点上升级Tomcat时忘了改Servlet包名排查了大半天。packaging为war时Maven默认会执行maven-war-plugin。如果你的web.xml是空的或者压根没有需要在插件配置里加failOnMissingWebXmlfalse/failOnMissingWebXml否则打包直接失败。2.3 web.xml与DispatcherServlet映射配置web.xml是Servlet规范的门面Tomcat先读它。SpringMVC的核心就是一个叫DispatcherServlet的前端控制器所有的请求都先到它手里再由它分发给具体的Controller。所以web.xml里最要紧的就是把这个Servlet注册上。servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingload-on-startup设置成1意思是容器启动时就把这个Servlet初始化好而不是等第一个请求进来才初始化。它的好处是启动阶段就能暴露配置错误不用等到用户访问才报500。我在调试期一律设成1宁可启动慢两秒也别让问题藏到运行期。url-pattern写/而不是/*这也是个高频翻车点。/*会把JSP请求也拦截进去导致视图解析完之后又绕回DispatcherServlet形成死循环或者404。写/的话JSP交给容器的默认Servlet处理静态资源也没问题。至于拦截器要拦哪些路径那是spring-mvc.xml里的事不在这里管。如果要同时加载Service、DAO这一层还需要一个ContextLoaderListener让它去读全局的applicationContext.xmlcontext-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener这里的分工要理清ContextLoaderListener建的是父容器管Service、事务、数据源DispatcherServlet建的是子容器管Controller、视图解析器、拦截器。子容器能看见父容器的Bean反过来看不见。这个父子关系搞明白很多Bean找不到的问题就迎刃而解了。2.4 applicationContext.xml的分层与扫描我把配置拆成两个文件applicationContext.xml管后端那层spring-mvc.xml管Web这层。拆开的好处是各自扫描范围明确不会互相干扰——尤其是事务代理和Controller混在一起扫描时很容易出现代理失效的问题。applicationContext.xml大致长这样context:component-scan base-packagecom.example context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan context:property-placeholder locationclasspath:application.properties file-encodingUTF-8/ bean iddataSource classorg.apache.commons.dbcp2.BasicDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean注意到那个exclude-filter了没有它的作用是让父容器不要扫Controller。如果不排除Controller会被扫描两次一次在父容器一次在子容器请求映射就可能出现重复或者覆盖报错信息还特别隐晦比如某个URL能访问但走的不是你以为的那个方法。这个坑我在生产的排查里遇到过花了不少时间才定位到。2.5 application.properties该放在哪、怎么被读到application.properties的位置很关键必须放在src/main/resources下编译后它会落到WEB-INF/classes里也就是classpath的根。这样classpath:application.properties才能找到它。我见过有人放在项目根目录然后抱怨读不到其实classpath里根本没有这个文件。配置内容通常是这样jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/demo?useUnicodetruecharacterEncodingUTF-8 jdbc.usernameroot jdbc.password123456这里有个编码细节容易被忽略。属性文件里的中文如果用的是JDK 9之前的Properties加载方式默认按ISO-8859-1解析读出来就是乱码。解决办法有两个一是context:property-placeholder上加file-encodingUTF-8二是干脆把中文拆到messages_zh_CN.properties这类国际化文件里用专门的编码方式处理。我习惯加上file-encoding省事。3. IDEA里的启动配置两种路线怎么选3.1 路线一外部Tomcat Local Server配置这条路是传统玩法适合真正的war工程。步骤本身不多但每一步都有坑。第一步Run→Edit Configurations→ 点左上角→ 找Tomcat Server→Local。这里第一个大坑来了IDEA社区版压根没有Tomcat Server这个选项。只有Ultimate版才内置。社区版用户必须装Smart Tomcat插件装完之后在里能看到Smart Tomcat配置项略有不同但思路一样。我在社区版上折腾过好几次一开始还以为是自己点错了地方。第二步在Application server那一栏点Configure指定你本地解压的Tomcat目录。注意是解压目录不是安装目录要能看到bin、conf、lib这些文件夹。选错了会提示找不到catalina脚本。第三步切到Deployment标签页点→Artifact→ 选择springmvc-demo:war exploded。选war exploded而不是war因为前者是解压后的目录形式支持热更新后者是打包好的war文件改代码就得重新构建。调试期一律用war exploded。第四步设置Application context也就是访问路径前缀。默认可能是/springmvc-demo_war_exploded这种又长又丑的我一般改成/或者/demo。注意这个值直接决定你浏览器里怎么敲地址。设成/demo那你的Controller映射是/hello完整地址就是http://localhost:8080/demo/hello。很多人报404就是因为前缀对不上。3.2 Server标签页里那些真正影响运行的参数Server标签页里我重点关注三个地方。一个是端口。默认8080被占用的话启动会直接报Address already in use: bind。换端口简单但我更建议找出占用进程因为端口冲突往往说明你之前有实例没关干净。Windows下用netstat -ano | findstr :8080找PID再去任务管理器结束。第二个是VM options。中文乱码问题基本靠它解决-Dfile.encodingUTF-8 -Xms256m -Xmx512m -XX:MetaspaceSize128m -XX:MaxMetaspaceSize256m内存参数怎么给我的经验是本地开发-Xms256m -Xmx512m够用元空间给128m起。元空间太小的话在项目类特别多的时候会报OutOfMemoryError: Metaspace这个错误看起来吓人其实就是类元数据放不下调大就行。第三个是On Update action这是热更新的开关。设成Update classes and resources之后你改了Java代码点一下重新编译Tomcat会尽量做增量更新不用重启。但要真生效还得配合两件事File→Settings→Build, Execution, Deployment→Compiler里勾上Build project automatically以及运行期允许自动编译。这个组合在新版IDEA里已经默认开着老版本需要手动开。3.3 路线二用Maven插件直接跑绕开IDEA配置如果你不想装插件、也不想配Artifact还有一条更省事的路用Tomcat的Maven插件直接在命令行跑。plugin groupIdorg.apache.tomcat.maven/groupId artifactIdtomcat7-maven-plugin/artifactId version2.2/version configuration port8080/port path/demo/path uriEncodingUTF-8/uriEncoding serverXml${project.basedir}/src/main/resources/tomcat-server.xml/serverXml /configuration /plugin然后命令行执行mvn clean tomcat7:run这条路的好处是环境干净不依赖IDEA的Artifact机制谁拉下代码都能一键跑。坏处是它内置的Tomcat版本比较老Servlet规范只到3.0如果你的项目用了Servlet 4.0的特性可能会在启动时报Unsupported major.minor version之类的错。另外它的热更新能力弱改代码基本要重启。我的做法是本地调试用IDEA外部Tomcat做CI或者快速验证时用这套插件命令。3.4 路线三Application启动类 内置Tomcat这条路是标题里application启动最直接的对应物。思路是把Tomcat嵌到代码里用main方法把整个容器拉起来好处是启动过程和部署方式都变成了纯Java不再依赖web.xml和外部容器。先写一个Web初始化类替代web.xmlpublic class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { Override protected Class?[] getRootConfigClasses() { return new Class[]{RootConfig.class}; } Override protected Class?[] getServletConfigClasses() { return new Class[]{WebConfig.class}; } Override protected String[] getServletMappings() { return new String[]{/}; } Override protected Filter[] getServletFilters() { CharacterEncodingFilter filter new CharacterEncodingFilter(); filter.setEncoding(UTF-8); filter.setForceEncoding(true); return new Filter[]{filter}; } }再写Web层的配置Configuration EnableWebMvc ComponentScan(basePackages com.example.controller) public class WebConfig implements WebMvcConfigurer { Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver resolver new InternalResourceViewResolver(); resolver.setPrefix(/WEB-INF/views/); resolver.setSuffix(.jsp); return resolver; } Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } }最后是启动类用内置Tomcat托管webapp目录public class Application { public static void main(String[] args) throws Exception { Tomcat tomcat new Tomcat(); tomcat.setPort(8080); tomcat.getConnector(); File docBase new File(src/main/webapp); tomcat.addWebapp(/demo, docBase.getAbsolutePath()); tomcat.start(); System.out.println(SpringMVC 已启动: http://localhost:8080/demo/); tomcat.getServer().await(); } }addWebapp这一步会去读WEB-INF/web.xml也会识别ServletContainerInitializer所以上面的WebAppInitializer能被自动发现。这条路最大的价值在于启动入口变成了一段你能打断点、能读懂的Java代码。排查问题时直接从main第一行往下走比在IDE的启动配置里猜要踏实得多。依赖上需要加org.apache.tomcat.embed:tomcat-embed-core和tomcat-embed-jasperJSP支持版本和Tomcat大版本对齐。顺带提一句如果你打算往Spring Boot方向走这套东西其实就是Spring Boot的雏形。Spring Boot帮你把Application启动类和自动配置做好了你只写SpringBootApplication就行。但理解了这个手写版本再去看Spring Boot的启动流程会顺畅很多。4. application配置与请求链路的细节打磨4.1 视图解析器、静态资源与编码过滤器视图解析器决定了Controller返回一个字符串之后容器去哪里找JSP。没配的话返回hello会被当成一个路径直接转发结果往往是404。基础配置bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ property nameviewClass valueorg.springframework.web.servlet.view.JstlView/ /beanprefix和suffix拼起来就是实际路径。返回hello最终转发到/WEB-INF/views/hello.jsp。放在WEB-INF下面还有个额外好处用户没法直接敲URL访问JSP必须经过Controller数据准备和权限校验就绕不过去了。静态资源要放行。mvc:annotation-driven/负责开启注解驱动mvc:default-servlet-handler/负责把CSS、JS、图片这类请求交回容器的默认Servlet。两个都要加只加一个的话要么注解不生效要么静态资源全404。这是SpringMVC配置里最典型的成对出现配置。编码过滤器也别漏。POST请求的中文乱码十有八九就是没配它filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingforceEncoding设成true的意思是不管请求头里声明的是什么编码一律按UTF-8处理。生产环境这么做是可以的因为你的前端页面本来就是UTF-8。4.2 类加载与依赖冲突的处理思路SpringMVC工程的依赖冲突表现出来往往是NoSuchMethodError或者NoClassDefFoundError。比如spring-core被两个不同版本引入了运行时加载了旧的那个方法签名对不上启动就炸。排查思路是这样的先在IDEA右侧的Maven面板里点Show Dependencies看依赖树里同一个artifactId有没有出现多次。有的话用exclusions把传递进来的那一份排掉或者用dependencyManagement统一锁定版本。这一步做完很多玄学错误会自己消失。还有一个容易被忽略的点是编译级别。工程如果用JDK 17编译但Tomcat跑在JDK 8上启动时会报Unsupported class file major version 61。解决办法是让两边对齐。在pom.xml里明确指定properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties同时在IDEA的Project Structure→Project里把SDK和Language level也一起改掉。IDEA有三处地方管编译级别Project、Modules、Compiler里的Java Compiler。三处不一致的时候会出现命令行能编译、IDEA里编译不过的怪现象。我一般三处一起看一遍再动手。4.3 Artifact配置为什么必须存在IDEA把编译产物和部署产物分成了两个概念。编译产物是target/classes部署产物是Artifact。Deployment里能选的Artifact如果列表是空的说明IDEA没给这个模块生成部署描述。生成方式File→Project Structure→Artifacts→→Web Application: Exploded→From Modules。在弹出的窗口里确认模块和主类IDEA会自动列出WEB-INF/classes和WEB-INF/lib。lib里要能看到Spring的jar看不到的话说明依赖没被识别成runtime依赖需要检查Maven导入是否完整。这一步很关键因为它决定了Tomcat实际加载的是哪份代码。我有一次改了代码但启动后没生效查了半天发现Tomcat加载的是另一个旧Artifact跟我改的模块不是同一个。从此我养成了一个习惯启动前先在Deployment里确认一遍Artifact名字改完代码后点Build→Rebuild Project别指望增量编译永远可靠。5. 踩坑实录与速查表5.1 几个真实踩过的典型问题问题一启动成功后访问404。出现频率最高。排查顺序是先看浏览器地址里的context path对不对再看web.xml里url-pattern是不是/然后看Controller上的RequestMapping路径有没有和前缀重复最后看component-scan的包路径有没有扫到Controller所在包。这四步走完九成404都能定位。问题二控制台报ClassNotFoundException找不到DispatcherServlet。大概率是依赖scope写错了或者Artifact里的WEB-INF/lib是空的。去Project Structure→Artifacts里看一眼就清楚了。问题三中文乱码。分三处查web.xml里的编码过滤器、Tomcat运行配置里的-Dfile.encodingUTF-8、JSP页面头部的pageEncoding。三处都设成UTF-8基本就没问题。IDEA控制台本身的乱码可以在Help→Edit Custom VM Options里加上编码参数。问题四改代码不生效必须重启才行。先确认On Update action设成了Update classes and resources再确认编译器自动构建开着。另外一个隐藏原因是改的是web.xml或者Spring的XML配置——这类资源的变更本来就不支持热更新必须重启容器这个没辙。问题五Tomcat启动到一半卡住日志停在某个位置不动。常见于数据库连接池初始化比如applicationContext.xml里配了数据源但数据库连不上连接池会一直重试。可以把初始连接数设小一点或者先注释掉数据源相关配置确认Web层能跑通之后再逐步加回来。5.2 常见问题速查表现象可能原因排查动作启动报Address already in use端口被占用netstat -ano找PID并结束进程或换端口访问报404context path 或映射路径不对核对 Deployment 里的 Application context 与 Controller 映射报ClassNotFoundException: DispatcherServlet依赖scope错误或Artifact缺lib检查 pom 依赖与 Artifacts 的 WEB-INF/lib报NoSuchMethodError同一依赖多版本冲突Maven 依赖树里排查并 exclusion中文乱码缺编码过滤器或编码参数检查过滤器、VM options、JSP pageEncoding报Unsupported class file major version编译JDK高于运行JDK统一 pom、Project Structure、Compiler 三处级别改代码不生效热更新未开启或改了XML开启 Update classes and resources配置改动需重启社区版找不到 Tomcat Server 选项Ultimate 才内置安装 Smart Tomcat 插件5.3 我在实际项目里总结的几条经验第一条先手工跑通再上工具。刚接触这套东西的时候别一上来就依赖IDEA的图形化配置。先用mvn clean package打个war手动拷到Tomcat的webapps目录启动startup.bat看能不能跑。这一步能跑通说明工程本身没问题剩下的事情全是IDE配置的事范围一下子就缩小了。第二条配置尽量外置。数据库地址、端口、日志级别这些别硬编码在XML里。用application.properties承载配合context:property-placeholder注入。改环境时只改一个文件不用动编译产物。这个习惯在后面上多环境部署的时候会省下大量时间。第三条启动日志从头读到尾。很多人启动失败之后只盯着最后一行异常看其实真正的线索往往在中间。比如它可能先报了某个Bean加载失败最后才抛出一大串堆栈。从第一个ERROR开始读效率高得多。第四条版本对齐先于一切。JDK版本、Servlet规范版本、Spring版本、Tomcat版本这四者是有对应关系的。动手之前先花两分钟确认这套组合是匹配的能避开一大半启动期的疑难杂症。第五条保留一份能跑的最小工程。我本地一直有个极简的SpringMVC工程就一个Controller、一个JSP、一份配置。遇到新环境或者新版本先拿它去试确认环境没问题再把真实项目放上去。这比在复杂项目里排查环境问题要轻松太多。最后再分享一个小技巧。启动失败的报错信息有时候会很长IDEA的控制台不好翻。我会在Run/Debug Configurations里把Logs标签页的日志输出到文件然后用编辑器搜关键字。另一个办法是在启动配置里设置Before launch把Build和Maven的clean动作挂上去每次启动前自动清理旧产物避免脏数据带来的误判。这些动作看着琐碎但积累下来能省掉不少重复排查的时间。