ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringMVC无绿色启动按钮?内嵌Tomcat+Application配置

2026/10/1 6:01:12 拓冰建站 浏览量
SpringMVC无绿色启动按钮?内嵌Tomcat+Application配置 SpringMVC 项目在 IDEA 里跑不起来很多人第一反应是我 Tomcat 配错了其实更常见的情况是项目压根没配出那个绿色的 Application 启动按钮。传统的 SpringMVC 是打成 war 包丢进 Tomcat 的 webapps 目录里跑IDEA 只是帮你把编译产物塞给一个外部的 Tomcat 实例中间隔了一层容器管理。可一旦你想用main方法直接拉起整个 Web 容器或者团队里有人用的是 IDEA Community 版它没有 Tomcat Server 运行配置这套玩法就卡住了。这篇东西就是围绕用 Application 配置启动 SpringMVC这件事把内嵌容器入口、IDEA 运行配置的每个勾选项、以及改造成 Spring Boot 启动的取舍从头到尾捋一遍。适合已经在写 SpringMVC、被部署流程折磨过、想把这套东西跑顺的人看小白也能跟着一步步抄。1. SpringMVC 项目为什么在 IDEA 里没有那个绿色启动按钮1.1 war 部署和 main 方法启动的本质差别先把这个认知拉平SpringMVC 本身不是一个能自己跑起来的东西它是一套建立在 Servlet 规范之上的 Web 框架必须寄生在一个 Servlet 容器里。传统做法是你写好web.xml配好DispatcherServlet的映射然后用 Maven 打成 war扔进 Tomcat 的 webappsTomcat 启动时读web.xml或者读ServletContainerInitializer把 Spring 的WebApplicationContext拉起来。整个过程里Tomcat 是宿主你的项目是客人。IDEA 里的 Tomcat Server 运行配置做的事情本质上是它自己启动一个 Tomcat 实例然后通过一个叫工件Artifact的机制把你的编译输出classes lib webapp 目录挂载成这个 Tomcat 的一个 context。你在 IDEA 里点那个红色停止按钮其实是通知 Tomcat 关掉。这套东西在 Ultimate 版里做得挺顺但它有个前提——你得有 Ultimate 版。问题就出在这。IDEA Community 版从设计上就不包含 Java EE / Web 相关的运行配置类型Tomcat Server、GlassFish、WildFly 这些统统没有。很多人在网上翻半天Tomcat 运行配置在哪最后发现自己的是社区版白折腾。这时候唯一的路就是绕开外部容器自己写一个 main 方法在里面 new 一个内嵌 Tomcat把 webapp 目录喂给它。这样一来IDEA 就把它当成一个普通的 Java Application绿色三角按钮自然就出来了社区版、Ultimate 版都能用。所以配置 application 启动 SpringMVC这件事核心不是配置而是给项目造一个 main 入口。配置只是把这个入口告诉 IDEA。1.2 IDEA 判定一个类能不能作为 Application 运行的规则IDEA 判断一个类能不能跑逻辑很朴素这个类里有没有一个签名合法的public static void main(String[] args)。只要满足编辑器左侧的行号栏就会出现那个绿色小三角右键菜单里也会多出 Run XXX.main()。但这里有几个容易踩的细节。第一main方法的参数类型必须是String[]写成String... args虽然语法上等价编译后签名一样IDEA 也认但如果你写成CharSequence[]或者别的它就不认了。第二类不能是抽象类也不能是接口。第三如果你的main方法所在类依赖了一堆编译期就能发现错误的类比如引用了不存在的包IDEA 会先把那个三角按钮标红或者干脆不显示直到你把编译错误消掉。还有一个隐性的坑模块的 source root 有没有被正确标记。如果你用 Maven 导入项目src/main/java应该被自动标记为 Sources Root蓝色。如果因为某种原因它变成了普通文件夹灰色那这个目录下的类在 IDEA 看来就不算项目源码main方法照样不会被识别。真遇到了右键目录 → Mark Directory as → Sources Root 就行。2. 用内嵌 Tomcat 给 SpringMVC 造一个 main 入口2.1 依赖怎么引、版本怎么挑内嵌 Tomcat 有一组专门的 artifact跟独立安装的 Tomcat 不是一回事。你要引的是tomcat-embed-core如果项目里用了 JSP还需要tomcat-embed-jasper。这俩是分开的很多人只引了 core结果启动之后访问 JSP 页面报 404 或者抛JasperException找半天找不到原因。在pom.xml里大概是这样properties tomcat.version9.0.83/tomcat.version /properties dependencies dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-core/artifactId version${tomcat.version}/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-jasper/artifactId version${tomcat.version}/version /dependency !-- 如果你的项目是 JSP JSTL还得补这两个 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies版本选择的逻辑是内嵌 Tomcat 的大版本要和你项目的 Servlet 规范对齐。SpringMVC 5.x 系列基于 Servlet 3.1/4.0对应 Tomcat 8.5 或 9.0如果你用的是 Spring 6 / SpringMVC 6那它已经迁到 Jakarta EE 9包名从javax.servlet变成jakarta.servlet这时候必须用 Tomcat 10而且代码里的 import 全要改。我见过有人 Spring 6 配 Tomcat 9启动时报ClassNotFoundException: javax.servlet.ServletContext一头雾水其实就是包名对不上。另外提醒一句tomcat-embed-core的 scope 如果你设成provided本地跑起来没问题但打包和在某些容器里会出问题。启动用的这个入口建议就用默认的compilescope别省这点体积。2.2 启动类的完整写法核心代码就几十行但每一行都有讲究import org.apache.catalina.Context; import org.apache.catalina.WebResourceRoot; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.webresources.DirResourceSet; import org.apache.catalina.webresources.StandardRoot; import java.io.File; public class ApplicationStartup { public static void main(String[] args) throws Exception { // 1. 指定端口 int port 8080; // 2. 建立 Tomcat 实例并设置基础目录 Tomcat tomcat new Tomcat(); tomcat.setBaseDir(target/tomcat-temp); tomcat.setPort(port); // 3. 触发 Connector 初始化这一步千万别漏 tomcat.getConnector(); // 4. 把 src/main/webapp 作为 web 应用的根 String webappDir new File(src/main/webapp).getAbsolutePath(); Context ctx tomcat.addWebapp(, webappDir); // 5. 让容器能找到 target/classes 里的类 File classesDir new File(target/classes); WebResourceRoot resources new StandardRoot(ctx); resources.addPreResources( new DirResourceSet(resources, /WEB-INF/classes, classesDir.getAbsolutePath(), /)); ctx.setResources(resources); // 6. 启动并阻塞 tomcat.start(); System.out.println(Application started at http://localhost: port); tomcat.getServer().await(); } }tomcat.getConnector()这一行是新手最容易漏的。在 Tomcat 8 之前直接setPort()然后start()也能起来但从某个版本开始内嵌 Tomcat 的 Connector 是惰性初始化的你不显式调用getConnector()它可能压根不创建连接器表现就是——启动日志一切正常端口就是访问不了。我自己第一次碰到时以为是防火墙查了半天。addWebapp而不是addContext也很关键。addWebapp会走完整的 Web 应用初始化流程会去读web.xml、处理ServletContainerInitializer、扫描注解。如果你用addContext那web.xml里配的DispatcherServlet就不会被加载Spring 容器根本起不来。这两个方法的差别值得在写的时候多留意一眼。2.3 webapp 目录和静态资源路径的坑用addWebapp之后容器对文档根目录的理解就是你传进去的那个路径。默认情况下src/main/webapp下的WEB-INF/web.xml会被读取WEB-INF/lib会被当成额外类路径。但注意编译产物在target/classes不在src/main/webapp/WEB-INF/classes所以上面第 5 步把 classes 目录映射成/WEB-INF/classes是必需的。那WEB-INF/lib里的依赖 jar 呢在 IDEA 里跑的时候依赖都在 classpath 上容器通过线程上下文类加载器就能找到一般不用手动映射。但如果你打的是 war 包然后用这套 main 方法去启动解压后的目录那 lib 目录会被自动识别。静态资源这块src/main/webapp/static/xxx.css这类文件addWebapp之后是能直接通过/static/xxx.css访问的因为容器默认对文档根目录下的非 WEB-INF 内容开放访问。但有个细节你的工作目录Working directory必须是项目根目录。如果 IDEA 默认把工作目录设成了模块目录比如xxx-module/那new File(src/main/webapp)就找不到会抛IllegalArgumentException: The main resource set does not contain the requested resource或者直接FileNotFoundException。这个下面配置章节会细说。3. IDEA 的 Run/Debug Configurations 到底每个选项管什么3.1 Working directory 和 Use classpath of module在Run → Edit Configurations里新建一个 Application 配置你需要填三样东西Main class、Use classpath of module、Working directory。Main class就是上面那个ApplicationStartup。Use classpath of module选你的项目主模块这个决定了运行时类加载器能看到哪些依赖。如果你是多模块 Maven 项目比如parent、web、service、dao那你应该选web模块也就是打 war 的那个而不是 parent。选错了的典型症状是NoClassDefFoundError而且报的类往往是你自己写的 service 类让人误以为是依赖没下下来。Working directory是我踩过最深的坑。IDEA 新建 Application 配置时工作目录默认是模块所在目录不是项目根目录。而你代码里写的是new File(src/main/webapp)这是相对路径相对于工作目录解析。如果你的模块目录嵌套了一层比如项目根/web/src/main/webapp那这个相对路径就解析到项目根/web/src/main/webapp看起来好像也对——但如果模块名和目录结构不完全一致用过 Maven 的都知道这种情况不少就会错位。稳妥做法是Working directory 明确设成项目根目录也就是pom.xml最外层那个所在目录。这样src/main/webapp和target/classes的相对路径才稳定。当然你也可以在代码里用绝对路径或者通过系统属性传入但硬编码绝对路径在团队协作时更麻烦不如把目录设对。3.2 VM options 和端口冲突的处理VM options这一栏是最容易被忽略又最容易出问题的地方。常见的几类配置编码问题-Dfile.encodingUTF-8。中文乱码很多时候不是页面编码的问题是 JVM 默认编码跟文件编码不一致。内存-Xms512m -Xmx1024m。Spring 容器 内嵌 Tomcat 起来之后默认堆可能偏小尤其是项目里 bean 多的时候。你要覆盖端口的话最好别硬编码在代码里而是写成-Dserver.port8081代码里用System.getProperty(server.port, 8080)读。这样换个端口不用改代码。端口冲突是启动时的经典问题。报错长这样java.net.BindException: Address already in use: bind。查端口的命令Windows 用netstat -ano | findstr :8080拿到 PID 之后tasklist | findstr PID看是谁占的Linux / macOS 用lsof -i :8080或者ss -ltnp | grep 8080。我一般会养成习惯本地开发固定用一个不常见的端口比如 18080避开那些经常被其他软件抢占的常用端口。还有个小技巧tomcat.setPort(0)会让系统随机分配一个可用端口然后你通过tomcat.getConnector().getLocalPort()拿到实际端口。做自动化测试或者并行启动多个实例时很有用。3.3 断点打不进去是什么情况配置好之后最常见的抱怨是能跑起来但断点打不进去。排查顺序是这样先看是不是用的 Run 而不是 Debug。绿色三角是 Run绿色小虫子才是 Debug这个失误人人犯过。再看断点有没有变成灰色带斜杠的圆圈。IDEA 里断点变灰表示当前类不可达或者字节码与源码不匹配。后者多发生在你改了代码但没重新 buildIDEA 用的还是旧字节码。Build → Rebuild Project一下通常就好。第三种情况比较隐蔽你的断点打在 Spring 的代理类调用的方法上而实际执行的是被代理对象的方法。比如你给某个 Service 加了TransactionalSpring 会生成一个 CGLIB 代理调用方拿到的是代理。断点打在接口方法上有时会失效得打在实现类上。第四种是内嵌容器下的类加载器隔离问题。addWebapp默认会给 web 应用创建一个独立的WebappClassLoader。如果你的启动类ApplicationStartup在系统类加载器里而 Spring 的 bean 在 webapp 类加载器里调试器的某些评估表达式可能表现异常。真遇到了可以试试tomcat.getHost().setAppBase(...)配合调整加载器策略或者干脆接受这个限制——大多数断点场景其实不受影响。4. 把 SpringMVC 改造成 Spring Boot 启动什么时候值得做4.1 判断标准项目规模和维护周期内嵌 Tomcat 这套方案能解决问题但它本质上是在模拟一个 Spring Boot 已经帮你封装好的东西。如果你维护的是个长期项目团队还要不断加人那用 Spring Boot 会更省心。判断标准我一般看三条第一项目是不是还在活跃迭代。如果只是偶尔改个 bug、半年碰一次那内嵌 Tomcat 的启动类就够了改造成本不值当。第二有没有既有的web.xml依赖。web.xml里塞了一大堆 Servlet、Filter、Listener 配置的迁移时要一个个翻译成 Java Config 或者 Spring Boot 的FilterRegistrationBean工作量不小。第三要不要打 war 部署到外部容器。有些客户环境规定必须部署到他们自带的中间件上那你就不能用 Spring Boot 默认的 jar 打包得保留 war 能力这时候改造要加SpringBootServletInitializer。我的经验是如果项目里web.xml的配置项少于 10 条且没有特殊的外部容器要求那就改。改完之后的启动体验、配置管理、依赖版本统一收益能覆盖迁移成本。4.2 web.xml 里的配置一项项怎么翻译迁移的核心工作是把web.xml里的声明式配置翻译成 Spring Boot 的编程式或注解式配置。给你一张对照表web.xml 里的配置Spring Boot 里的等价做法context-param指定 Spring 配置文件位置PropertySource或application.ymlDispatcherServlet的servlet和servlet-mappingspring-boot-starter-web自动注册无需手动配context:component-scan启动类上的SpringBootApplication自动扫描CharacterEncodingFilterserver.servlet.encoding.*配置项自定义FilterBean FilterRegistrationBean或ComponentOrder自定义ListenerBean ServletListenerRegistrationBeanViewResolverJSP 场景application.yml配spring.mvc.view.prefix/suffix并加tomcat-embed-jasper举个具体的例子。原来web.xml里可能这么写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-mapping改造之后这段整体删掉Spring Boot 的DispatcherServletAutoConfiguration会自动帮你注册一个映射到/的DispatcherServlet。你需要做的只是把spring-mvc.xml里的视图解析器、拦截器配置改成WebMvcConfigurer的实现类Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/static/**, /login); } Override public void configureViewResolvers(ViewResolverRegistry registry) { registry.jsp(/WEB-INF/views/, .jsp); } }拦截器的excludePathPatterns一定要逐条核对。web.xml时代很多人是靠mvc:exclude-mapping配的迁移时漏掉一条就会导致静态资源被拦截器拦住页面能打开但样式全丢或者登录页被重定向死循环。这个坑我自己踩过排查了半天以为是静态资源路径问题。4.3 迁移后最容易出问题的三个地方JSP 支持。Spring Boot 默认不打 JSP因为内嵌 Tomcat 对 JSP 的支持需要额外依赖而且官方不太推荐。你要在pom.xml加tomcat-embed-jasperscope 用provided因为打 war 部署到外部容器时外部容器自己有然后把 JSP 放到src/main/webapp/WEB-INF/views/下注意 Boot 要求 JSP 必须在src/main/webapp下放src/main/resources里是找不到的。静态资源映射。SpringMVC 时代通常在spring-mvc.xml里配mvc:resources mapping/static/** location/static/ /。Boot 里默认已经把classpath:/static/、classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/映射到了根路径。如果你原来的静态文件放在src/main/webapp/static/下那要注意 Boot 默认不管这个目录需要自己加映射或者在WebMvcConfigurer里补一条。数据库和事务配置。原来applicationContext.xml里的DataSource、SqlSessionFactory、事务管理器Boot 里都能靠 starter 自动配置搞定但你得把配置项搬到application.yml并且确认包扫描路径对得上。特别是 MyBatis 场景MapperScan的路径写错会直接导致NoSuchBeanDefinitionException报错信息还不太直观。5. 启动阶段的报错排查链路5.1 类找不到先把报错信息读完整ClassNotFoundException和NoClassDefFoundError是两个不同的东西别混。前者是加载时压根找不到这个类通常是依赖没引、坐标写错后者是编译时存在运行时找不到常见于依赖传递丢失或者类加载器隔离。排查时把报错信息完整读一遍尤其是Caused by那几层。举个真实例子报错栈最后一行是Caused by: java.lang.ClassNotFoundException: javax.servlet.jsp.jstl.core.Config你就能直接定位到是 JSTL 依赖没引而不是 Tomcat 配置问题。很多人只看了第一行的ServletException然后去查容器问题方向就跑偏了。还有一个内嵌容器特有的情况org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat]]。这个往往不是 Tomcat 的问题而是你的 Spring 容器初始化抛异常了被 Tomcat 包装了一层。继续往下看Caused by真正的错误通常在里面比如 bean 创建失败、Autowired找不到实现类。5.2 端口、路径、编码这三类高频问题我把这几类问题整理成一张表方便对照现象可能原因处理方式Address already in use: bind端口被占用换端口或结束占用进程启动无报错但访问 404addWebapp路径传错或DispatcherServlet未注册检查 webapp 目录与 web.xml页面中文乱码JVM 编码与文件编码不一致加-Dfile.encodingUTF-8并在响应头设 charset静态资源全部 404拦截器拦截或资源目录未被映射检查excludePathPatterns与资源映射启动卡住无响应await()前有阻塞操作或数据库连接超时检查连接池配置与启动日志JSP 报JasperException缺tomcat-embed-jasper补依赖确认 scope其中启动卡住无响应这个特别值得说。tomcat.getServer().await()本身是阻塞的这是正常的它让主线程挂着不退出。但如果你在start()之前做了数据库连接、远程调用之类的事情而这些操作超时了那卡住的位置就在start()之前。这时候打印日志很关键我在启动类里一般会加几行System.out.println标记每个阶段比如 准备 webapp 目录完成、容器启动完成这样卡在哪一步一目了然。5.3 一套可复用的排查顺序踩了这么多坑之后我固定了一套排查流程从外向里逐层验证第一步确认 Java 版本和依赖版本匹配。mvn dependency:tree看一眼有没有冲突的 servlet-api很多时候是两个版本的javax.servlet-api同时存在导致容器行为异常。第二步确认工作目录和路径。在启动类最开头打印System.getProperty(user.dir)看看实际工作目录是不是你以为的那个。这一行几乎不占成本能省掉大量猜测。第三步确认 webapp 目录里确实有WEB-INF/web.xml。addWebapp如果找不到 web.xml有些版本会静默跳过初始化你的 Spring 容器就不启动表现就是所有请求都 404。这种情况可以在addWebapp之后打印一下ctx.getDocBase()验证。第四步把 Spring 的日志级别调成 DEBUGlogback.xml或者log4j2.xml里给org.springframework开 DEBUG。这样DispatcherServlet的初始化过程、handler mapping 的注册情况都会打出来很多请求进来了但没进 Controller的问题一看日志就知道是映射没匹配上。第五步最小化复现。如果实在定位不到把项目里无关的 Controller、Service 先注释掉只留一个最简单的接口看能不能通。能通就一点点加回来二分定位。这个方法笨但有效尤其是接手别人项目的时候。我个人在实际操作中的一个体会是启动类的路径配置和 IDEA 的工作目录设置这两件事要一起看。因为它们之间是联动的单看任何一边都可能觉得没问题但组合起来就是错的。我现在的习惯是启动类里的所有路径都基于System.getProperty(user.dir)拼接然后在 IDEA 里把 Working directory 明确设成$PROJECT_DIR$IDEA 里可以直接用这个宏这样不管项目放在哪个盘、模块叫什么名字路径都稳。另外启动日志里我会固定打印一行当前工作目录和webapp 绝对路径出问题时第一眼就能看到比翻半天配置文件快得多。