同事把一个十来年前的工程从版本库里拉下来拖进 IDE,一按构建,控制台红了一片,排在最前面的就是这句:Syntax error, parameterized types are only available if source level is 1.5 or greater。很多人的第一反应是"代码里泛型写错了""JDK 装少了",然后开始重装 JDK、换 SDK、清缓存,折腾一晚上问题还在。其实这条报错的指向非常明确——编译器被要求用 1.4 或更低的语言级别去解析带有泛型的代码,泛型是 Java 1.5 才引入的语法,翻译官看不懂新词,自然掀桌子。这不是业务代码的锅,是编译配置的锅。
这篇内容围绕 Java 编译级别(source level)展开,把这条报错的成因、排查链路、各环境下的具体改法、以及改完之后仍然复发的几种情况全部拆开讲清楚。适合三类人看:刚接手历史工程被红波浪线吓到的新人、本地能编译但 CI 上挂掉的开发者、以及需要给团队定编译版本基线的负责人。看完之后你应该能做到:拿到这条报错,五分钟内定位到是哪个配置文件在作祟,并且知道为什么这么改,而不是照着搜索结果乱点。
1. 这句报错究竟卡在哪一环
1.1 措辞本身携带了定位线索
先把报错原文抄一遍:Syntax error, parameterized types are only available if source level is 1.5 or greater。注意它的用词——"Syntax error"、"parameterized types"、"source level"。这三个词组合在一起,其实已经暴露了编译器身份。
如果你用的是标准的javac(不管是命令行还是 Maven 直接调用),在低语言级别下碰到泛型,吐出来的话术是另一套,形如generics are not supported in -source 1.4,并附带一句(use -source 5 or higher to enable generics)的建议。而parameterized types(参数化类型)这个说法、加上only available if ... or greater这种句式,是 Eclipse JDT 编译器(ecj)的标志性措辞。
这个细节决定了排查方向。看到这句报错,你应该优先怀疑:这是个 Eclipse 系工程,或者构建链里有环节在调用 ecj,又或者工程被导入时把 Eclipse 的编译配置文件一起带进了新 IDE。我见过太多次,工程本身用的是标准 javac 构建,但版本库里躺着一个.settings/org.eclipse.jdt.core.prefs,新 IDE 导入时读了它,于是直接从 1.4 起步。
顺带说一个容易混淆的点:parameterized types指的是List<String>这种带类型参数的形式,但它不是孤例。同一个语言级别下,增强 for 循环、自动装箱拆箱、可变参数(String... args)、静态导入、枚举,统统属于 1.5 引入的语法。所以有时候报错不是卡在泛型那一行,而是卡在一个for (String s : list)上,编译器先撞到谁就报谁。定位的时候不要只盯着泛型关键字搜,把整批新语法特征都当成嫌疑对象。
1.2 source level 和 JDK 版本是两回事
这是整个问题里最核心的认知差。很多人把"我装了 JDK 17"和"我的工程用 17 的语言级别编译"划等号,这两件事在配置上是完全解耦的。
打个比方。JDK 是一整套工具箱,里面有锤子、扳手、电钻,越新的箱子工具越全。source level是你告诉编译器"请只用 1.4 时代允许的工具干活"。工具箱里明明有电钻,但你说只准用螺丝刀,那遇到需要打孔的地方(泛型语法),编译器只能停下来告诉你:这个工具在你的授权范围之外。
这套机制存在的意义是真实的:你要发布一个能在老运行环境上跑的程序,就必须保证生成的字节码不超出那个环境的理解能力。所以编译器提供了-source控制"接受哪种语言语法",-target控制"生成哪个版本的 class 文件"。两者可以不一致,虽然绝大多数情况下应该保持一致。
关键结论:报这个错,说明当前生效的 source level 小于 1.5(也就是 1.3 或 1.4 这一档),跟你本机装了 JDK 8、11 还是 21 没有直接关系。你重装多少次 JDK 都不会好,因为问题在配置里不在安装包里。
1.3 谁把语言级别按到了 1.4
既然不是 JDK 的问题,那 1.4 是哪来的。根据我处理过的案例,来源集中在下面几个位置,按出现频率从高到低排:
- Maven 编译插件的默认值。2.x 时代的编译器插件,在没有显式配置时会用极低的默认 source 级别(1.3 那一档),3.0 之后抬到了 1.5。所以一个只写了依赖、没写编译配置的老
pom.xml,配上老插件版本,天然就会踩雷。 - Eclipse 工程的
.settings/org.eclipse.jdt.core.prefs。这个文件记录着 compliance、source、target 三个值,老工程里可能是 1.4。它是跟着工程走、进版本库的,所以会"传染"到每一个新拉代码的人。 - IDEA 的 Project language level。从 Eclipse 工程导入时,IDEA 会读取
.classpath和.settings,把语言级别一并继承过来。 .classpath里的 JRE 容器声明。有些老工程的.classpath里写死了J2SE-1.4这样的执行环境名,导入时会连带影响语言级别判断。- 父 POM 或 profile 里的统一配置。多模块工程里,子模块的配置可能被父级覆盖,你以为改了子模块,实际生效的是父级。
这五处里,前两处占了绝大多数。所以动手之前先做一件事:在工程根目录搜一下1.4这个字符串,看看它藏在哪些配置文件里。这一步能省掉后面大量试错。
2. 三层配置的定位顺序:先搞清谁说了算
2.1 项目级、构建工具级、IDE 全局级的优先级关系
同一个工程的语言级别,可能同时被三个层级的配置影响,它们的生效顺序经常让人困惑。我用一句话概括:构建工具执行编译时,看的是构建脚本里的配置;IDE 做语法分析和即时错误提示时,看的是 IDE 工程模型里的配置。这两套是并行运行的,任何一套不对都会让你看到红色的报错,但报错的来源不同。
更具体一点。当你在 IDEA 里点构建、走的是 Maven 或 Gradle 时,真正执行的是构建工具的编译任务,此时pom.xml里的maven.compiler.source(或插件的<source>)说了算,IDE 的 language level 只是影响编辑器里那些波浪线。反过来,如果你用的是 IDE 自己的增量编译(Build Project),那就是 IDE 工程模型里的 Target bytecode version 在起作用。
这解释了一个非常典型的困惑场景:改了 pom 之后,mvn compile通过了,但编辑器里还是满屏红。因为 pom 改了构建工具那一层,IDE 工程模型那一层没同步——需要让 IDE 重新导入(Reload / Reimport)才会刷新。
反过来也有:IDE 里没红,命令行一编译就炸。那就是 IDE 工程模型恰好设对了,但构建脚本里的话事人还是 1.4。
2.2 用一条命令确认当前真正生效的级别
别靠猜。Maven 项目直接跑:
mvn -X compile 2>&1 | grep -i -E "source|target|release"-X是 debug 模式,会打印插件实际使用的参数。你能看到编译器插件收到的-source和-target真实取值。如果这里显示 1.4,那问题就在 Maven 这一层,去查 pom。
想看得更细,可以显式让编译器打印:
mvn compile -Dmaven.compiler.showWarnings=true -Dmaven.compiler.showDeprecation=trueGradle 项目则用:
./gradlew compileJava --info 2>&1 | grep -i -E "source|target|release"至于 Eclipse 系工程,最快的办法是直接看文件:
cat .settings/org.eclipse.jdt.core.prefs | grep compiler正常应该看到三行,形如:
org.eclipse.jdt.core.compiler.source=1.8 org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8如果这里是 1.4,那答案就摆在眼前了。
2.3 症状与责任层的对照表
排查时最有价值的不是"怎么改",而是"该改哪"。下面这张表是我按实际处理经验整理的对照关系,可以对着症状缩小范围:
| 现象 | 大概率责任层 | 首查位置 |
|---|---|---|
命令行mvn compile就报错 | 构建工具配置 | pom.xml的 compiler 插件与 properties |
| 命令行正常,IDE 编辑器里报语法错 | IDE 工程模型 | Project Structure 的 language level |
| IDE 构建报错,但编辑器不报 | IDE 编译器设置 | Compiler 的 Target bytecode version |
| 只有一个模块报错,其他正常 | 模块或父 POM 覆盖 | 子模块 pom、parent 继承链、profile |
| 拉下来的工程别人能编我不能编 | 环境差异或本地缓存 | JDK 版本、增量编译缓存、~/.m2 |
| 换了 IDE 之后突然报错 | 配置文件被自动识别 | .settings、.classpath、.iml |
最后一行值得单独强调。Eclipse 和 IDEA 互相导入工程时,都会去读对方的配置文件,而且是"能读多少读多少"。这本来是为了平滑迁移,副作用是把对方的历史包袱也一起搬过来了。我现在的习惯是:接手任何跨 IDE 迁移的工程,第一件事就是把.settings、.classpath、.project、.iml、out/、target/这些统统扫一遍,看清楚版本号再动手。
3. 各环境下的具体改法
3.1 Maven:properties 和插件配置的优先级
Maven 项目里有两处能设语言级别,优先级是插件配置高于 properties。所以一个常见的坑是:你在<properties>里写了maven.compiler.source=1.8,但插件配置块里忘了删旧的<source>1.4</source>,结果后者赢了。
最省心的写法是在 properties 里统一声明,让它同时驱动 source 和 target:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.release>8</maven.compiler.release> </properties>这里同时写 properties 和release看起来重复,其实是刻意为之。maven.compiler.release需要编译器插件 3.6.0 及以上才生效,老插件会直接忽略它;把source和target也写上,是给老插件兜底。等到确认插件版本够新之后,可以只留release一个。
用插件配置块的话,长相是这样:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <release>8</release> <encoding>UTF-8</encoding> </configuration> </plugin>顺带提一个重要判断:如果工程里没有显式声明编译插件的版本(只写了artifactId没写version),Maven 会用一个默认版本,行为不可控。我的做法是永远显式写版本号,并且养成习惯——升级编译插件版本本身就可能改变默认语言级别,这是"什么都没改,突然编译不过"的常见诱因。
3.2 Gradle:sourceCompatibility 与 options.release
Gradle 里最直接的是sourceCompatibility和targetCompatibility,它们会转成 javac 的-source和-target:
java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } tasks.withType(JavaCompile).configureEach { options.encoding = "UTF-8" options.release = 8 }注意options.release需要 Gradle 6.6 以上。它的好处后面第 5 节会展开讲,简单说就是比单设 source/target 更严格可靠。
如果用 Kotlin DSL,写法是:
java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" options.release.set(8) }Gradle 还有个容易忽略的坑:sourceCompatibility写在java {}块里会被应用到所有JavaCompile任务,但如果你手写了某个任务并单独设置了options.compilerArgs,可能覆盖掉全局设置。排查时用前面提到的--info看实际参数最靠谱。
3.3 Eclipse 系老工程:.settings 里那三个键
这类工程的特点是配置文件进版本库,所以必须改文件而不是只改界面。直接编辑.settings/org.eclipse.jdt.core.prefs:
org.eclipse.jdt.core.compiler.source=1.8 org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8三个键要一起改,缺一个就可能出现"语法能过但字节码还是老版本"的诡异状态。改完之后在 IDE 里执行一次 Clean + Rebuild,别用增量构建。
同时检查.classpath,看有没有把执行环境写死成老版本:
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.4"/>里面那个J2SE-1.4就是罪魁之一。把它换成对应的新版本容器名,或者干脆用 IDE 里的 JRE 容器引用。
改这几处的时候有个强烈建议:先跟团队确认能不能动这些文件。有些团队的.settings是刻意锁定的,用来保证所有人编译行为一致;擅自改动会引发合并冲突。这种情况下正确的做法是推动团队统一定一个新基线,而不是自己本地改完提交。
3.4 IDEA:三处配置必须联动
IDEA 里的语言级别分散在三个地方,只改一处经常不够。
第一处是工程模型层:File → Project Structure → Project,看SDK和Language level。这里是全局默认,模块没有单独设置时会继承它。
第二处是模块层:同一窗口的Modules,每个模块可以有自己的Language level,会覆盖工程级设置。多模块工程的报错往往出在这里——工程级是 8,某个模块还是 4。
第三处是编译器层:Settings → Build, Execution, Deployment → Compiler → Java Compiler,这里的Target bytecode version决定生成 class 的版本。这一处和语言级别是分开的两件事,前者管"能写什么语法",后者管"生成什么版本"。很多"编辑器不报错但运行时报版本不兼容"的情况,就是这里没对齐。
用 Maven 工程的话还有第四处:Settings → Build, Execution, Deployment → Build Tools → Maven → Importing,里面有个JDK for importer。这一项影响的是 Maven 工程模型的导入解析,设错会导致 IDEA 读不懂你的 pom。
实操顺序建议:先改 pom,然后在 Maven 面板点 Reload,让 IDEA 自动同步工程模型;接着检查 Project Structure 里的模块级别有没有被同步成正确值;最后确认 Compiler 里的 target bytecode。三步都对齐,基本就不会再反复。
3.5 裸 javac:-source、-target 与 --release 的取舍
直接调 javac 的场景(脚本构建、小 demo、临时验证)最容易踩坑,因为没有任何工具帮你兜底。
javac -source 8 -target 8 -encoding UTF-8 -d out $(find src -name "*.java")在 JDK 9 及以上,这条命令会附带一条警告:
warning: [options] bootstrap class path not set in conjunction with -source 8意思是:我按 8 的语法和字节码标准来,但我用的引导类库还是当前 JDK 的。这会导致一个隐蔽问题——你可能不小心用上了新 JDK 才有的 API,编译不报错,跑到 8 的环境上直接NoSuchMethodError。
正确的做法是用--release:
javac --release 8 -encoding UTF-8 -d out $(find src -name "*.java")--release 8会同时约束语法级别、字节码版本和可见的 API 集合,用一份"8 时代的标准库快照"来校验你的代码。多打几个字,省下的排查时间是以小时计的。
还有一个 Windows 上的小坑:上面命令里的$(find ...)是 shell 语法,cmd 里不认。稳妥做法是用参数文件:
# 先生成文件列表 find src -name "*.java" > sources.txt javac --release 8 -encoding UTF-8 -d out @sources.txt@sources.txt这种参数文件方式跨平台一致,路径里有空格也不会崩,我现在基本都用这个。
4. 改完还在报错:几个反复咬人的场景
4.1 版本控制把旧配置又带回来了
这是最让人抓狂的一类。你在本地把.settings改好了,编译通过,提交代码,第二天拉最新,又红了。原因通常是:别人的本地环境跑了一次 IDE 的自动配置,或者某次合并把旧版本文件合了回来。
排查办法很直接,看文件历史:
git log --oneline -- .settings/org.eclipse.jdt.core.prefs如果看到最近还有提交,说明这个文件仍在被某些人的环境反复改写。治本方案有两个:一是把这类 IDE 私有配置文件从版本库里移除,加进忽略列表,改成每人本地生成;二是如果团队强依赖它保证一致性,就要在提交前加一道检查。
我倾向于第二种方案配上一道自动检查。比如在 CI 脚本里加一步:
grep -q "compiler.compliance=1.4" .settings/org.eclipse.jdt.core.prefs && exit 1看着简陋,但能挡住绝大多数误提交。更规范的做法是用一个小的校验脚本检查所有编译相关配置文件的版本号是否一致。
4.2 增量编译缓存与注解处理器
配置全改对了,报错还在,先别急着怀疑配置。IDE 和构建工具都有增量编译机制,它记着"哪些文件编译过了、用什么参数编译的",参数变了不一定触发全量重编。
清理方式按环境区分:Maven 用mvn clean,Gradle 用./gradlew clean,IDEA 用Build → Rebuild Project(注意不是 Build),Eclipse 用Project → Clean并勾选Build automatically之后的全部重建。另外 IDEA 的out/目录、Eclipse 的bin/或target/classes都可以直接删。
注解处理器是另一个隐藏雷区。Lombok、MapStruct、各类代码生成器都在编译期介入,它们自己也是用某个 Java 版本编译出来的。如果注解处理器的版本和你的 source level 差太远,可能出现"配置明明对,但生成的代码报语法错"的怪现象。遇到这种情况,先把注解处理器依赖升到与编译级别匹配的版本再试。
我踩过最迷惑的一次是:本地全绿,CI 上同一份代码报泛型语法错。最后查出来是 CI 的构建镜像里缓存了上一次的target/目录,而那次构建用的是一份出错的配置。加了mvn clean之后问题消失。从那以后我的 CI 脚本第一条永远是清理,宁可多花三十秒。
4.3 别把"语法级别错"和"类文件版本错"混为一谈
Java 的编译期版本问题有两大家族,症状相似但根因完全不同,混淆了会浪费大量时间。
家族一就是本篇讨论的:语法级别不匹配,报错里出现source level、generics are not supported、parameterized types这类词,属于"编译器不认识你写的语法"。
家族二:类文件版本不匹配,报错长这样:
java.lang.UnsupportedClassVersionError: com/example/Foo has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0或者编译期版本:
class file has wrong version 61.0, should be 52.0这类属于"运行环境看不懂你生成的字节码",对应的是-target或--release设置。看到class file version这个关键词,就该去查 target 而不是 source。
两者的关系可以用一句话记住:source 管"进",target 管"出"。报错里提到语法、泛型、语言特性,查 source;提到 class file version、major version,查 target。混淆方向的话,改半天也改不到点子上。
顺便给一张查版本时常用的对照表,看到数字能快速反应过来:
| class 文件 major version | 对应 Java 版本 |
|---|---|
| 49 | 5 |
| 50 | 6 |
| 51 | 7 |
| 52 | 8 |
| 53 | 9 |
| 55 | 11 |
| 61 | 17 |
| 65 | 21 |
注意 54 对应的是 10,56 到 60 分别对应 12 到 16,中间那几档日常很少见,但看到 55 别再以为是 9。
4.4 多模块工程里的覆盖链条
单模块工程的配置就那几个位置,多模块一上来复杂度翻倍。父子 POM 的继承、profile 的激活条件、dependencyManagement与插件的版本继承,任何一环都可能让实际生效的配置和你看到的文件不一致。
排查多模块工程的标准动作是逐层打印生效值:
mvn help:effective-pom -pl 模块名 > effective.xml然后在新生成的文件里搜maven-compiler-plugin,看最终合并后的配置是什么。这一步能直接暴露"子模块没配、父模块配了旧值"这种隐蔽问题。我看过太多案例,开发者信誓旦旦说"我明明改了",effective-pom 一打出来,生效值跟他改的地方完全不是同一处。
Gradle 多模块相对的对应手段是打印任务属性:
./gradlew :子模块名:properties | grep -i compat如果工程里有 profile 或条件配置,还要确认当前激活的是哪个。Maven 可以用mvn help:active-profiles看,别默认以为默认 profile 一定生效——环境变量、settings.xml、命令行参数都可能改变激活结果。
5. 版本基线怎么定才不反复踩
5.1 优先用 --release 而不是分开设 source/target
前面提过--release更严格,这里说清楚为什么值得作为团队标准。
只设-source 8 -target 8的时候,编译器接受 8 的语法,生成 8 的字节码,但它校验 API 时用的仍然是当前 JDK 的类库。于是在 JDK 17 上编译一个目标为 8 的工程,你完全可能写出调用了 17 才有的方法的代码,编译一路绿灯,部署到 8 的环境上运行时爆炸。这种问题排查成本极高,因为编译期毫无征兆。
--release 8相当于告诉编译器:"假装你现在就是 JDK 8,用 8 的类库快照来校验。"API 用超了当场报错。代价是编译稍微慢一点点,以及需要使用较新的 JDK(9 及以上),这个代价我认为完全值得。
Maven 里对应maven.compiler.release,Gradle 里对应options.release,命令行对应--release。三个入口一处原则,团队里统一成这条规矩,能消灭一大类"本地好使线上炸"的问题。
有一点必须提醒:--release不接受1.8这种带点的写法,只认8。而-source和-target两个都认(1.8和8等价)。混着配的时候容易写错,我见过写成<release>1.8</release>然后报release version 1.8 not supported的,排查了半天才发现是格式问题。
5.2 团队基线的确定方法与落地
版本基线不该由某个人拍脑袋定,但也不该无限期讨论。我的经验是抓三个约束条件,取交集:
第一是运行环境。生产服务器上的 JRE 版本是硬约束,编译目标不能超过它。这一条通常是决定性的。
第二是依赖库的要求。翻一下关键依赖的文档,看它们要求的最低 Java 版本。有些库新版本直接要求 11 或 17,这时候基线就被顶上去了。
第三是新 JDK 对老 source 的支持窗口。这一条很多人忽略。javac 的老版本支持是逐年在收缩的,-source 6、-source 7在较新的 JDK 上已经直接被拒绝,报Source option 6 is no longer supported。所以你就算坚持用 1.6 的基线,迟早也会因为开发机被迫升级 JDK 而卡死。具体哪些级别还支持,直接跑javac --help看--release那一行的说明,比自己记版本号可靠。
三条取完交集,把结果写进三处:父 POM 或根构建脚本、CI 配置、工程根目录的一份说明文档。写文档这一步别省,我见过太多团队口头约定"我们用 8",结果新人拉下来一看是 6,问谁都说不知道。
5.3 一个容易被忽略的隐形成本
编译级别这件事还有一层隐性影响:它决定了你能用哪些语言特性,进而影响代码风格。1.8 有 lambda 和 Stream,1.6 没有;8 之后到 11 引入了var(局部变量类型推断),17 引入了 record 和 sealed class。基线压得太低,团队写代码会一直停留在老范式里,新人看着别扭,老人也提不起兴致重构。
我的看法是:只要运行环境允许,基线尽可能往前推。不是为了用新语法炫技,而是新语法在很多场景下确实能减少出错概率——var减少重复类型声明带来的改一处漏一处,record 天然不可变,switch 表达式强制穷举分支。这些收益是实打实的。
当然前提是第一步评估到位。升级基线和升级运行环境要一起规划,只升一边必然出事。
6. 三次排查复盘与一份可复用清单
6.1 老工程导入后满屏红
某次接一个历史工程,从版本库拉下来导入,编辑器里几乎每个文件都有红波浪线,排头的就是这条参数化类型的报错。
第一步不是改配置,是确认来源。我在工程根目录跑了一遍搜索,发现.settings/org.eclipse.jdt.core.prefs里三个键全是 1.4,.classpath里还挂着J2SE-1.4的容器声明。工程本身是 Maven 结构,但 pom 里没写任何编译配置,用的是编译器插件默认值。
处理顺序是:先改 pom,显式声明maven.compiler.release为 8 并锁死插件版本;然后让 IDE 重新导入 Maven 工程,让它按 pom 重建工程模型;最后把.settings里的三个键改成一致值,作为兼容残留清理掉。三步做完,红波浪线消失,mvn clean compile通过。
复盘时最有价值的发现是:只要 pom 明确写了编译级别,IDE 导入时就会以 pom 为准,.settings的干扰会小很多。所以治本的关键是让构建脚本成为唯一可信源,IDE 配置文件作为可清理的中间产物。
6.2 本地能过 CI 不过
另一次是相反的形态。本地mvn clean compile一路绿灯,推上去 CI 直接失败,报的也是语言级别相关的错。
对比环境差异:本地 JDK 17,CI 镜像 JDK 11;本地 Maven 版本较新,CI 用的是镜像里自带的。关键线索是 CI 打出的编译器参数里source显示 1.5——这说明 CI 上生效的编译器插件版本比本地老,因为老版本插件的默认 source 就是 1.5,而 1.5 恰好支持泛型,所以报的不是参数化类型那个错,而是另一个更隐蔽的错误:某处用了var,1.5 不认识。
这次教训是:构建工具的版本必须锁死。Maven 用maven-wrapper固定版本,Gradle 用gradle-wrapper固定版本,编译插件显式写version。三样都做齐之后,本地和 CI 的编译行为才真正一致。之前"本地好使"只是运气好,撞上了新版本插件的宽松默认值。
6.3 一份可以直接抄的检查清单
把上面的经验收拢成一张清单,遇到这类报错按顺序过一遍,基本不会漏:
- 看清报错措辞。有
parameterized types、source level,优先查 source 配置;有class file version,转去查 target 配置。 - 全局搜一次
1.4,把散落在各配置文件的旧值找出来,范围包括pom.xml、build.gradle、.settings/、.classpath、.iml。 - 跑
mvn help:effective-pom或gradle properties,确认最终生效值,不要相信自己看到的局部文件。 - 检查构建工具版本是否被固定,包括 wrapper 和插件
version。 - 检查 JDK 版本与目标级别是否兼容,新 JDK 上过老的 source 级别可能已被拒绝支持。
- 清理所有增量编译产物,做一次全量重建。
- 对齐三处:构建脚本、IDE 工程模型、IDE 编译器 target bytecode。
- 确认改动已提交且不会被版本库里的旧配置覆盖回来。
最后分享一个我自己坚持的习惯:新接手任何工程,第一件事不是看代码,是先跑一次全量构建,然后看一眼构建日志里的编译器参数。三十秒的事,能提前暴露九成的环境问题。踩过几次坑之后我越来越觉得,Java 生态里那些看起来玄学的"配置问题",本质上都是同一件事——有多个地方能设同一个值,而你不确定哪个在生效。把"确认生效值"变成肌肉记忆,这类问题就不再是玄学了。