刚把IDEA从社区版换到旗舰版时,我干过一件傻事:每新建一个类,都要手动删掉IDE自动生成的注释,再改成团队规定的格式;每写一个测试类,都得复制粘贴之前的import和框架注解;Controller层加接口时,更是把鉴权、日志、参数校验那些样板代码从头敲一遍。直到有一天,我实在受不了这种重复劳动,花了一个下午把IDEA里的文件模板彻底研究了一遍。结果就是,后面所有新文件的创建从"每次手工整理"变成了"一键生成"。这篇就聊聊在IDEA里创建文件模板这件事,包括入口在哪里、变量怎么写、实战怎么配、以及那些文档里不会写但你必须知道的坑。
这篇文章适合谁?凡是日常用IDEA写Java(或其他JVM语言)的开发者,尤其是团队里对代码格式和文件头有统一要求的朋友。不管是想顺手提升个人效率,还是想把团队规范固化到IDE里,这篇文章都能给你一套可以直接落地的东西。
1. 先搞清楚:文件模板到底帮你省了哪些事
先说一个很多人容易忽略的点:IDEA里的"模板"其实分了两种,一种是Live Templates(实时模板),输入psvm按Tab就能生成public static void main的那种;另一种就是标题里说的File and Code Templates(文件与代码模板),它管的是你通过右键 New -> Java Class 这类入口新建文件时,IDE给你的初始内容长什么样。
这两者的区别很关键。Live Templates是你"写代码时"的快捷键,File and Code Templates是你"建文件时"的坯子。很多人混着用,结果在新建文件时找不到自己配的缩写,其实是因为压根走错了入口。本文要讲的,是后者。
那文件模板到底能帮你省什么事?以最普通的Java项目为例,新建一个Class时,IDE默认会生成这样的内容:
java复制public class Foo {
}
就一行,干干净净。但实际工作里,一个类真的只要这样吗?团队规范要求文件头有版权声明;接口类要求写上@author和@since;实体类要求实现Serializable并生成serialVersionUID;Controller方法要求带@Tag和@Operation注解。这些如果都靠每次新建后手动补,一天建十几个文件就是十几个重复动作,还容易忘。
文件模板做的事情,就是把"新建后手动补"变成"新建时自动带"。你配置一次,以后每次建文件,IDE会按照你定义的模板把初始内容铺好。更进阶一点,结合模板变量,还可以自动带上当前日期、当前用户名、包名、类名,甚至根据文件类型做条件判断。这一套下来,新文件的初始状态就直接是"接近可提交"的水平。
所以我的建议是:不要把文件模板当成一个花哨功能,它就是你的效率基础设施。 配好之后,新建文件这个高频操作能省下80%的重复劳动。下面直接讲操作。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 文件模板的入口与界面拆解:别被四个Tab吓到
打开IDEA,按Ctrl+Alt+S(macOS是Cmd+,),进入设置,在左侧搜索框输入File and Code Templates,你会看到一个长这样的界面,里面分了几个Tab:Files、Code、Includes、Templates(不同版本可能略有差异)。
2.1 四个Tab各管什么
| Tab | 作用 | 使用频率 |
|---|---|---|
| Files | 新建文件时使用的模板,比如Class、Interface、Enum、Record等 | 最高,日常最常用 |
| Code | 代码生成片段,比如生成Getter/Setter、equals/hashCode时的样式 | 中,改动需谨慎 |
| Includes | 公共片段文件,可以被其他模板引用,最典型的就是File Header.java |
高,团队规范常改这个 |
| Templates | 用于Web开发的设计时模板,比如HTML、JSON等文件类型的初始内容 | 低,取决于项目类型 |
注意看,Files和Includes是联动的。默认情况下,Class模板的内容是:
java复制#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
package ${PACKAGE_NAME};
#end
#parse("File Header.java")
class ${NAME} {
}
这里出现了两个关键语法:#if是Velocity模板的条件判断,#parse("File Header.java")表示引入在Includes标签页里定义的那个公共片段。你点开Includes -> File Header.java,会看到默认内容:
java复制/**
* @author Your Name
*/
所以很多人误以为自己改了Class模板就行,但实际上头部注释应该去改File Header.java——因为它被所有文件类型共用。你要是只在Class模板里加头注释,那Interface、Enum这些文件就还是老样子,等于白弄。
2.2 为什么IDEA要拆成Files和Includes两层
这是IDEA设计上很聪明的地方。它把"文件类型特有内容"和"所有文件公共内容"拆开。文件类型特有内容,比如类的声明关键字、继承关系、包名,这些随文件类型变化;公共内容,比如版权声明、作者信息、日期,这些所有文件都应该带上。把公共部分单独抽出来放到Includes里,你就只需要改一处,所有文件类型同时生效。
我见过不少同事不知道这个设计,在Class模板里加了头注释,又去Interface模板里加一遍,还去Enum模板里加一遍,不仅重复劳动,后面想改一下头注释的内容,得三个地方同时改,早晚有漏改的时候。正确做法就是只改File Header.java,一劳永逸。
3. 模板变量与Velocity语法:让模板"活"起来的核心
模板不可能只是一个死文本,否则和手动复制粘贴也没区别。IDEA的模板引擎基于Apache Velocity,支持变量替换、条件判断、循环、引入子模板等能力。但大部分场景你只需要掌握变量替换和条件判断两个语法。
3.1 内置变量:直接拿来就用
在Files模板里,最常用的内置变量有这些:
| 变量 | 含义 | 适用场景 |
|---|---|---|
${PACKAGE_NAME} |
当前文件所在的包名 | 自动生成package语句 |
${NAME} |
新建文件时输入的文件名(不含扩展名) | 类名、接口名 |
${FILE_NAME} |
完整文件名(含扩展名),新版本IDEA可用 | 文件头注释里写文件名 |
${DATE} |
当前日期,格式为yyyy/MM/dd | 文件头日期 |
${TIME} |
当前时间,格式为HH:mm | 文件头时间 |
${YEAR} |
当前年份 | 版权声明(强烈推荐用这个) |
${MONTH} |
当前月份,两位数字 | 日期相关 |
${DAY} |
当前日,两位数字 | 日期相关 |
${USER} |
当前系统用户名 | 作者信息兜底 |
${PROJECT_NAME} |
当前项目名 | 文件头项目标识 |
${DIR_PATH} |
当前文件所在目录路径 | 特殊标识场景 |
这里特别说下${NAME}和${FILE_NAME}的区别。新建文件时,IDEA会先弹一个输入框让你填文件名,比如你输入UserService,然后选类型Interface。此时${NAME}就等于UserService,而${FILE_NAME}在新版IDEA里会等于UserService.java。模板里如果你用${NAME}.java也能拼出完整文件名,但如果IDEA版本支持${FILE_NAME},直接用更省事。
3.2 自定义变量:默认值怎么来
光有内置变量还不够,有时候你想要一个"自定义的"值,比如每次新建文件时要手动填的作者姓名。IDEA的模板变量对话框右下角有一个Edit variables按钮,点开可以给变量设置默认值和表达式。
举个例子,如果模板里写了${AUTHOR},IDEA会弹一个框:
- Name: AUTHOR
- Default value: 可以填字符串,比如
zhangsan - Expression: 可以填Groovy脚本,比如
groovyScript("return 'zhangsan'")
更常见的是,我们会把${USER}作为兜底,再用表达式去读取本机配置:
code复制groovyScript("System.getProperty(\"user.name\")")
实际用下来,我的建议是:作者信息用表达式从系统读取,但要在团队规范里约定提交前检查。避免出现谁建的文件就写谁的名字、审核时说不清的情况。
3.3 Velocity条件判断:模板可以更智能
你可以在模板里写#if和#end来控制某些内容只在特定条件下出现。比如默认的Class模板就有一句:
code复制#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
package ${PACKAGE_NAME};
#end
意思就是:如果包名非空,才生成package语句,否则不生成。这在有些场景下非常实用。例如你可以做一个模板,要求Controller类自动生成@RestController和@RequestMapping,但Entity类不生成——那就不该用同一个模板,而是用不同文件类型分别配置。
另一种用法是配合#parse引入公共片段。你可以在Includes里建一个Company Header.java,内容是自己公司的版权声明;再在Files的Class模板里写#parse("Company Header.java")来引入它。这样版权声明只有一份,改起来方便。
3.4 Velocity语法中最常用的几条
#if (条件)...#end:条件为真时输出中间内容#if...#else...#end:条件为假时走else分支#set($变量 = 值):设置变量,例如#set($now = ${YEAR} + "年")#parse("模板名"):引入另一个模板的内容#foreach($item in $list)...#end:循环,实用频率较低
说明:#set在IDE模板里偶尔会用,比如你想拼接一个带格式的日期字符串;#foreach用得少,但如果你有"生成一组枚举值"之类的需求,可以试试。
4. 实战:搭一套真正能用的Class和测试模板
理论讲了一堆,下面直接上干货。以我所在的团队为例,我们的规范是:所有新建的Java类文件头必须有版权声明、作者、创建日期;Controller层接口必须自动生成@Tag和@Operation注解所需的基础注释;工具类必须自动挂上@UtilityClass(Lombok)。下面逐步操作。
4.1 第一步:改公共文件头注释
打开Settings -> File and Code Templates -> Includes,新建一个文件,名字叫Company Header.java,内容写成:
java复制/**
* Copyright (C) 2024-${YEAR} Your Company. All rights reserved.
*
* @author developer
* @since ${DATE}
*/
然后回到Files标签,把Class模板改成:
java复制#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
package ${PACKAGE_NAME};
#end
#parse("Company Header.java")
#if (${NAME} != "")
/**
* ${NAME}
*/
#end
class ${NAME} {
}
注意这里我加了一个新的#if块:只有当文件名非空时才生成类注释。这样避免某些极端情况下IDE报空指针。
顺便把Interface模板也改了,让它同样包含#parse("Company Header.java")。Enum、Record同理。
4.2 第二步:创建一个Controller专用模板
我们在Files标签里点+号,新建一个模板,Name填Controller Class,File name填${NAME}.java,扩展名填java。模板内容:
code复制#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
package ${PACKAGE_NAME};
#end
#parse("Company Header.java")
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 接口控制器
*
* @author developer
* @since ${DATE}
*/
@Api(tags = "${NAME}")
@RestController
@RequestMapping("/${NAME}")
public class ${NAME} {
}
保存后,在项目任意包下右键New -> Controller Class,输入UserController,回车,你会看到生成的内容已经带了包名、版权头、Swagger注解、RestController注解和基础RequestMapping。这时候你要做的只是往方法里填业务代码。
有人会问:为什么RequestMapping路径直接用类名?因为模板不知道你想要的RESTful前缀,这是约定,生成后手动调整就行。如果你想要更智能的,可以用Velocity的#set把类名转换成小写中划线风格,但那属于高级玩法,容易翻车——先说结论:简单胜于完美,先生成再改比配一个复杂模板靠谱。
4.3 第三步:测试类模板也别放过
如果你用JUnit 5,默认新建Test类的时候,IDEA会给你一个空类。但团队往往要求测试类继承一个BaseTest,或者统一用MockitoExtension。在Files标签新建一个模板:
code复制#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
package ${PACKAGE_NAME};
#end
#parse("Company Header.java")
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* ${NAME}
*/
@ExtendWith(MockitoExtension.class)
class ${NAME} {
@Test
void test() {
// TODO
}
}
保存后,右键New -> Test Class建测试文件就都有了。
4.4 专属提示:Edit variables按钮怎么用
在Files模板列表下方有一个Edit variables按钮(需要选中某个模板后点击)。以Class模板为例,如果你的模板里写了${AUTHOR},那么点Edit variables,会看到AUTHOR这一行,在Default value里填developer,在Expression里可以留空或者写groovyScript("return 'developer'")。
我推荐把作者这类信息统一写死成团队机器人账号,而不是用系统用户名。因为团队成员电脑用户名可能五花八门,比如admin、test、zhang_san,生成到代码里很难看,还容易触发代码格式检查错误。
5. 模板迁移与团队复用:别让规范只存在你一个人电脑里
配置好模板之后,最大的问题就是:怎么让团队里其他人也用上?总不能一个个发截图让人照着配。IDEA提供了两种方式:导出配置包,或者直接把配置目录交给版本管理。
5.1 方式一:Export Settings / Import Settings
打开File -> Manage IDE Settings -> Export Settings(旧版本在File -> Export Settings,macOS在IntelliJ IDEA -> Settings里),勾选File and Code Templates,导出成jar包。把jar包发给同事,对方在File -> Manage IDE Settings -> Import Settings里选择这个jar包,导入时勾选模板项,重启IDEA就生效了。
这个方式适合一次性初始化,缺点是后续如果模板更新了,得重新导一次再发一遍。
5.2 方式二:模板配置文件直接纳入版本库
IDEA的模板配置文件存放在<项目根目录>/.idea/fileTemplates/下,包括includes子目录。举个例子,你的项目是Git仓库,可以把整个.idea/fileTemplates/目录提交到Git里。其他同事拉取代码后,IDEA会自动读取这些模板——前提是他们的IDEA配置里开启了Settings Sync或者至少没有显式覆盖。
我实测下来,这种方式的同步效率最高。你只需在模板文件更新后提交一次代码,team里所有人pull代码重启IDEA就是最新的。注意:这种方式依赖项目级的.idea目录被团队接受纳入版本管理。有些团队会忽略掉整个.idea目录,那就没法用这一招了。
这里有个小坑:IDEA的模板缓存可能不会立即刷新。改完.idea/fileTemplates/里的模板文件后,同事那边最好执行一下File -> Invalidate Caches / Restart,让IDEA重新加载模板。否则偶尔会看到旧模板的残留。
5.3 方式三:结合Settings Repository
如果你开启了Settings Repository(设置仓库)功能,可以把IDEA全局配置(包括File and Code Templates)同步到私人仓库或公司内网Git。改完模板后Push给远端,其他人Pull即可。
三种方式里,我个人最推荐方式是二(模板文件入库),因为它是项目级的,不同项目可以有不同的模板规范,不像全局设置那样一刀切。多项目并行时,项目A用一套Controller模板,项目B用另一套,各不干扰。
6. 用文件模板必须避开的几个坑
这部分是血泪经验。文档和教程里很少提,但实际用的时候十有八九会遇到。
6.1 坑一:改了系统内置模板导致升级被覆盖
直接修改IDEA内置的Class、Interface模板,是大多数人的第一反应。但IDEA大版本升级时,这些内置模板有可能会被重置为默认值。如果你在里面写了很多团队定制内容,升级后一夜回到解放前。
我的建议是:能新建模板就新建,不要覆盖系统模板。 在Files标签点+号新建一个My Class也好,或者给系统模板加内容时,保留一份原始备份。如果非要覆盖系统模板,先把原始内容复制到本地一个txt里,升级后发现被重置还能快速恢复。
6.2 坑二:日期格式不符合团队规范
${DATE}生成的日期格式是yyyy/MM/dd,比如2024/06/18。但国内很多团队的规范是yyyy-MM-dd。这就尴尬了。好在Velocity支持#set来做格式化,你也可以直接用${YEAR}-${MONTH}-${DAY}自己拼:
java复制#set($date = "${YEAR}-${MONTH}-${DAY}")
注意:${MONTH}和${DAY}默认是两位数字,所以拼出来的格式是2024-06-18,没问题。如果你想要不带前导零的格式,就得用Groovy表达式了,但日常场景两位数字就够用了。
6.3 坑三:在Includes文件里引用自定义变量
这里有个非常容易踩的雷。你可能会很自然地想在Company Header.java这个公共片段里用到${NAME},心想这样头注释里就能带上类名了。但实际上,Includes文件被#parse引入时,是拿不到这个变量的。
为什么会这样?因为Includes文件本身是独立的模板,IDE在渲染它的时候有一个独立的上下文。如果要从外部传入变量,你得用#set在Files模板里先设置变量,然后传给include文件。比如:
在Class模板里写:
text复制#set($className = ${NAME})
#parse("Company Header.java")
然后在Company Header.java里才能用${className}。这套逻辑对新手来说非常反直觉,我当初也是排查了半天才明白,原来不是变量名写错了,而是作用域问题。
6.4 坑四:模板变量和Live Templates混淆
前面说过,文件模板(File and Code Templates)和实时模板(Live Templates)是两个完全不同的体系。文件模板的变量是${NAME}这种,Live Templates的变量是$NAME$这种(两个美元符包裹)。不要在一个体系里用另一个体系的语法,结果一定是变量原样输出,不会替换。
比如Live Templates里写$DATE$就有效,在文件模板里写$DATE$,生成结果就是文本$DATE$,而不是日期。新手极其容易在这上面栽跟头。
6.5 坑五:模板里出现了多余的缩进和空行
Velocity模板对空行和缩进是敏感的。你在模板里写了几个空行,生成出来的文件就会出现几个空行;你在模板每行前面加的空格,也会原样出现在生成文件里。所以写模板时,排版要克制,不要为了美观加大量空行和缩进,否则生成的每一行代码都可能开头带空格,直接触发Checkstyle报错。
正确的姿态是:模板内容严格对齐最终要生成的文件格式。比如package语句后面必须空一行再写import,那模板里也老老实实空一行。如果发现生成的文件空行太多,先检查模板里是不是有多余空行。
6.6 坑六:文件名输入框里的后缀问题
新建模板时,File name字段如果填了${NAME}.java,那么新建文件时你输入的名字里就不需要带.java后缀。但有些同事习惯输入UserService.java,结果生成出来的文件叫UserService.java.java,看着很蠢。模板设计时可以在File name列只填${NAME},不填扩展名,IDEA会根据Extension字段自动补。或者干脆在File name里就写死${NAME}.java,然后约定新输入时只填类名。
我个人推荐后者:File name填${NAME}.java,Extension留空。这样新建文件时输入UserService,IDE会通过模板生成一个UserService.java文件。如果用户不小心输入了.java后缀,文件名就会异常,IDE会提示,比生成一个错误文件要友好。
7. 把模板变成团队技术规范的载体
模板配置到位后,它就不再只是一个效率工具,而是一个团队规范载体。团队里新来的同事不需要反复问"我们类的头注释长什么样""Controller注解要加哪些",新建一个文件,所有该有的都有了。
这里分享几个我在实际推进模板落地时的体会:
第一,模板里的内容要克制。 不是所有东西都放进模板才是好事。我曾经见过有人把一整个"用户管理"的所有CRUD接口都写进模板里,结果每次新建文件都要删掉一堆不相关的方法,效率反而更低了。模板应该只负责"这批文件几乎一定需要"的内容,比如头注释、依赖导入、类注解。业务方法不要放。
第二,模板的变更要走版本管理。 我们的模板文件现在直接放在项目仓库里,每个改动都走Merge Request。这样模板的改动历史、原因都清清楚楚,不会出现"这个模板谁改的,为什么和我记忆里不一样"的讨论。
第三,模板和代码审查配合。 如果团队里有人新建文件后又手动删掉了模板生成的@author,那大概率是故意的,要么是模板内容有问题,要么是他有特殊场景。与其让模板"万能",不如保留人工微调的空间。
第四,定期复盘模板。 隔几个月回头看,可能有的模板生成的代码已经过时了,比如框架版本升级后注解路径变了,这时候更新模板比更新一堆历史文件要重要得多。因为模板面向的是未来所有会创建的代码。
最后再分享一个小技巧:如果你在文件模板里用了#parse("Company Header.java"),但某天想临时看看这个公共片段单独渲染出来是什么效果,可以右键该模板选择Preview,IDEA会弹一个预览窗口,填好变量值就能看到最终结果。这个功能排错很有用,尤其是当你怀疑变量没替换成功时,比新建一个真实文件去验证要快得多。
我现在新建任何Java文件,生成的初始内容基本就是规范化的最终形态,很少再动删除和补充的功夫。文件模板这个功能,配好了就一劳永逸,值得花一个下午认真弄一次。
