刚转后端那几年,我最怕听到一句话:“你画个UML图,讲讲这块结构。”不是不想画,而是老式画图软件里拖框、连线、对齐要花掉大把时间,类一多整个图就乱成一团。后来我养成了一个习惯:UML图尽量在IDEA里出,让图直接跟着代码走。用IDEA画UML图这个事,说穿了就是三步——让代码生成图、把图整理干净、再把图放进该去的地方。这篇文章我不讲教科书概念,直接把我每天的实操流程写出来:从示例项目的类结构设计,到右键生成类图,再到包依赖、文本脚本补时序、最后踩过的几个坑。适合正在用IDEA做日常开发的Java工程师,也适合接手旧项目想快速看结构的同学。
1. 为什么我最后选了在IDEA里直接出图,而不是单独开一个绘图软件
1.1 先想清楚:你需要的是一张“生成图”而不是“手绘图”
很多人的第一反应是“画UML图当然要用绘图工具”,于是每次做设计文档都要从头拖方框。我以前的负责人说过一句话:手绘UML图的保质期通常不超过两周。代码一改动,图上某个箭头就过时了,下次评审又得重画。
IDEA里的Diagram思路完全不同。它不是一个手动画板,而是一个“代码结构的实时投影”。你选中工程里的类、接口、包,IDEA会解析当前源码之间的继承、实现、依赖和关联,自动生成图。代码改完,重新刷新一次,图就跟着变了。这正是IDEA出图最核心的价值:省掉“同步图与代码”这件事。
1.2 这套流程到底能解决什么问题
我梳理了一下,下面这些场景都会用到它:
- 接手旧代码:项目历史长、包结构乱,先选中某个包生成包图,比靠肉眼一屏屏翻代码快得多。
- 模块重构前盘点:把核心类拖进一张类图,哪些类互相依赖、哪些接口实现一堆,一眼就看出来。
- 代码评审:评审前生成一张局部类图或包图,贴到评论里,省得在会议室用嘴比划半天。
- 新人培训和方案讲解:讲架构不再靠PPT里的静态大图,现场生成图,被问到哪一块就展开哪一块。
最典型的一次是我接手一个订单模块。代码不到二十个类,但service层互相引用,我看源码看得很痛苦。当时我直接选中service包,右键生成包图,半分钟内就看到一个A到B、B又回到A的循环依赖。这个循环光靠人眼追代码,可能要翻上十几分钟;生成图之后,红线箭头的方向就是证据。
1.3 IDEA里常见的三种画图路径,怎么选
在IDEA里出UML图,其实有三条路可以走。我把它们放在一起比较:
| 出图方式 | 数据来源 | 维护成本 | 最擅长的事情 |
|---|---|---|---|
| 内置Diagrams功能 | 当前源码 | 很低,代码变了重新生成即可 | 类图、包图、依赖关系图 |
| 文本式UML插件 | 手写UML描述脚本 | 中等,脚本要随设计维护 | 时序图、用例图、状态图等动态/行为图 |
| 手工绘图软件 | 手动绘制 | 很高,每次改动都要重画 | 对外汇报的示意架构图 |
我个人对这三条路的看法很明确:只要图的内容是想表达“当前代码里真实存在的结构”,就优先用内置Diagrams;只要图的内容是“类之间怎么协作、先调用谁后调用谁”,就用文本式脚本;只有那种给客户或高层看的概念图,才值得用手工工具画一张精致的示意图。
2. 前置准备:先搭一个能被IDEA清晰识别的类关系Demo
2.1 一个订单模块的示例代码结构
我们要演示,就得有一个结构清晰的例子。下面是我经常用来演示的订单模块,类不多,但恰好覆盖了继承、实现、组合、依赖这几种常见关系。你在自己的工程里不复刻它也没关系,理解这个结构就行。
// 客户实体 public class Customer { private Long id; private String name; private List<Order> orders = new ArrayList<>(); public void addOrder(Order order) { orders.add(order); order.setCustomer(this); } }// 订单实体 public class Order { private Long id; private Customer customer; private List<OrderItem> items = new ArrayList<>(); private PaymentService paymentService; public BigDecimal totalAmount() { return items.stream() .map(OrderItem::subtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } public void pay() { paymentService.pay(totalAmount()); } }// 订单条目 public class OrderItem { private Product product; private int quantity; private BigDecimal price; public BigDecimal subtotal() { return price.multiply(BigDecimal.valueOf(quantity)); } } // 商品 public class Product { private Long id; private String title; private BigDecimal price; }支付部分加一层接口和实现,方便演示UML里的“实现”箭头。
public interface PaymentService { boolean pay(BigDecimal amount); } public class CardPayment implements PaymentService { @Override public boolean pay(BigDecimal amount) { // 模拟卡支付 return true; } } public class WalletPayment implements PaymentService { @Override public boolean pay(BigDecimal amount) { // 模拟钱包支付 return true; } }最后是应用服务层,它负责协调领域对象。
public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; public OrderService(PaymentService paymentService, OrderRepository orderRepository) { this.paymentService = paymentService; this.orderRepository = orderRepository; } public void createOrder(Customer customer, List<Product> products) { // 创建订单的省略逻辑 } } public interface OrderRepository { void save(Order order); }把这些类放到一个Maven或普通Java工程里,只要能编译通过,IDEA就能把它们之间的结构画出来。
2.2 IDEA是怎么识别出这些关系的
理解这一点特别重要。IDEA不是靠类名去猜,而是靠符号解析。它读取源码后,先建立一套语法树模型,再扫描每个类里的关键字和成员声明:
- 一个类写了
extends,就会生成一条继承关系; - 一个类写了
implements,就会生成一条实现关系; - 一个类的字段类型是另一个类,就会生成关联关系,常见的比如
Customer里的List<Order>; - 一个方法参数或返回类型引用到别的类,通常体现为依赖关系。
所以图上的每一根线,在源码里都有对应依据。这也就解释了一个常见现象:如果代码有很多反射调用、动态代理、按字符串加载的类,IDEA的图是画不出那些隐藏关系的。图是代码的“结构投影”,不是运行时调用关系的完整还原。
2.3 生成图之前,先确认代码处于可编译状态
这个步骤很多人会跳过,然后发现图越画越少。只要工程里有红标错误,IDEA对类型符号的解析就会被打断,一些本该出现的箭头会直接消失。
我的习惯是打开图之前先按一次构建按钮,让IDEA完成一轮类型检查。如果项目很大,构建不方便,至少也要确保当前准备生成图的代码块没有报错。带着一堆红线去画图,画出来的东西不完整,很容易误导后续判断。
3. 核心操作:生成一张类图,并把它从杂乱调到能见人
3.1 打开类图的三种姿势
在IDEA里,生成类图的入口至少有三种,我按使用频率排一下:
- 在左侧Project窗口选中一个类文件或一个目录,点右键,找到Diagrams或“图”这一项,再选择Show Diagram。如果只在临时弹窗里看,就选Show Diagram Popup。前者会把图固定在编辑区,我常用于认真分析;后者适合随手扫一眼,两秒钟就关掉。
- 如果你的IDEA装了常用快捷键方案,弹出式类图通常是
Ctrl+Alt+Shift+U,直接在新页签打开类图则常用Ctrl+Alt+U。不同版本的默认键位可能不一样,你可以在设置里搜索Keymap,再搜Diagram把快捷键调成自己顺手的方式。 - 在类编辑器内部空白处点右键,同样会出现Diagrams菜单项,适合正在读一个类时突然想看它和谁有关联。
第一次在超大项目里生成图,IDEA会卡一两秒,这是在做符号索引。你需要先给IDEA一点时间把项目索引建好,否则第一次出图会很慢。
3.2 图上的元素和关系线怎么看
生成的类图里,每个类是一个方框,通常分三栏:类名、字段、方法。你可以通过点击类框右上角的折叠按钮收起某一部分。比如只想看类之间的结构,可以把方法和字段都收起来,图面会清爽很多。
关系线是读图的关键,我用一个表总结一下常见图元的含义:
| 图上的表现 | 含义 | 对应的源码 |
|---|---|---|
| 实线空心三角形箭头 | 继承 | class B extends A |
| 虚线空心三角形箭头 | 实现 | class B implements A |
| 带箭头的实线 | 关联或依赖 | 字段类型、方法参数、返回类型 |
| 带实心菱形或空心菱形的线 | 组合或聚合 | 集合字段、局部变量持有 |
不同版本的IDEA画出来的依赖线可能有多有少。因为字段依赖、方法参数依赖、返回类型依赖是三类不同的依赖,IDEA默认只展示一部分。如果你想看更完整的关系,可以在图上空白处右键,找到与依赖显示相关的选项,把方法依赖、参数依赖等开关打开。
3.3 把布局整理顺手的几个操作
刚生成的图通常不够好看,因为IDEA默认的自动布局可能不是你脑子里的层次。下面几个动作我几乎每次都会用到:
- 自动布局:在图上空白处右键选择Layout,尝试不同布局方式。层次布局适合有明确上下层关系的模块,正交布局适合依赖比较密集的场景。
- 隐藏噪音成员:在设置或Diagram工具栏里过滤setter、getter、构造函数、重写方法。这些成员会让类框变得很长,但对你理解结构几乎没有帮助。
- 只看邻居:选中几个核心类,右键选择“显示邻居相关类”,图上会自动收缩成只和这几个类产生直接关系的局部视图。这是最实用的降噪手法。
- 分层展示:需要看继承体系时,在接口或父类上右键,可以让实现类或子类以某种方式展开。
整理布局的目的是让读者一眼看到重点。我的原则是:先全量生成,心里有数;再按需裁剪,做一张能贴进文档的干净图。别指望一张图上能看到所有细节,那不叫图,叫地铁线路表。
3.4 导出图片:PNG、SVG还是直接复制
图整理好之后,常见的交付方式有三种。
第一种是直接复制到剪贴板。在图上选中,按复制快捷键或通过右键菜单Copy to Clipboard,然后粘贴到聊天工具、文档或评审评论里。这种方式最快,适合沟通。
第二种是导出成图片文件。在图的工具菜单里选Export to File,通常可以选PNG、JPEG、SVG。我的建议是:如果只是放进Word或Markdown文档,PNG够用;如果要印刷或投放到大屏,SVG更稳,因为它不缩放不失真。
第三种是保存成IDEA的图文件,方便下次打开继续编辑。但这会引入额外的存档文件,我的经验是只对高频更新的核心模块这么做。
关于导出清晰度有一个小坑:PNG导出时的分辨率和你当前缩放比例有关。如果图很小就导出,放大后文字会糊。我习惯先把图放大到文字清晰可读的状态,再导出PNG。SVG不受这个限制。
4. 进阶画法:包依赖图、动态时序图,以及两种画法的配合
4.1 从单个类图扩展到包依赖,一眼看出分层是否健康
只画单张类图还不够,我经常要回答的问题是“这个模块的分层对不对”。这时候应该生成包图。
操作方法和类图类似:在项目窗口选中某个包,右键选择Diagrams里的Show Diagram。IDEA会把包之间的依赖关系画出来,包的层级和依赖箭头都有体现。控制器依赖服务层,服务层依赖仓储层,这是健康的;如果仓储层反向依赖服务层,包图上会出现一根值得警惕的箭头。
我在实际项目里用这个功能查过很多次循环依赖。有一回新同事提了一个改动,说只是加了一个小方法,结果在包图上一看,新增的依赖让repository包直接指向了service包,整张包图都开始绕圈。要是看单个文件,压根意识不到问题有这么大。包图非常适合这种“从局部改动静默演变成整体结构恶化”的检查。
4.2 类图画不出动态行为,用文本式UML插件补时序图
类图和包图解决的是“结构问题”,也就是谁跟谁有关系。但很多设计讨论的核心是“行为问题”,比如用户提交订单之后,订单和支付服务到底怎么流转。这时候需要用文本式UML插件。
IDEA的插件市场里有不止一款支持文本化UML的工具,你搜UML、Diagram这类关键词就能看到。安装之后,在项目里新建一个插件能识别的UML描述文件,写一段类似下面的脚本:
@startuml actor 顾客 顾客 -> OrderService : createOrder(customer, products) OrderService -> OrderRepository : save(order) OrderRepository --> OrderService : order OrderService -> PaymentService : pay(amount) PaymentService --> OrderService : result OrderService --> 顾客 : 订单结果 @enduml保存之后,插件会在编辑区旁边实时渲染出一张时序图。如果你想改调用顺序,直接调脚本里的行序,图会跟着变。这个体验比手工画时序图舒服太多,因为时序图一改就是全局重排,手工画起来特别痛苦。
文本脚本写多了以后,你会把它当成一种“可执行的文档”:放在工程里,别人看到脚本就能改图。这种文件非常适合跟着代码一起进版本库,评审时直接打开看,不用额外生成一堆静态图。
4.3 什么时候用内置图、什么时候用文本脚本
我给自己定的规则是这样的:
- 面向已有代码的结构分析,一律用IDEA内置Diagrams,因为数据来源准确,不用手写任何描述。
- 面向未来设计的结构讨论,可以先手写一个文本式类图脚本,快速调整类名和关系。
- 只要涉及时间顺序,比如先请求A再请求B、超时重试都画不出来的场景,就用文本式时序图。
- 只有给外部人员看的、不依赖具体代码的高层架构图,才动手工工具。
内置图最大的优势是“不撒谎”,文本脚本最大的优势是“灵活”。两者搭配使用,基本覆盖了日常开发里90%的画图需求。
5. 我在IDEA里画UML图时最常踩的四个坑
5.1 坑一:关系线只有你喜欢的那几条,其他箭头都消失了
刚接触的人最容易疑惑:明明A类的字段里有B,图上却没有箭头。原因很可能是IDEA对“依赖”的展示是分类的,字段依赖、方法参数依赖、返回类型依赖默认的展示策略不同。
另外一个更隐蔽的原因是,代码里的关系藏在接口和动态绑定背后。看前面例子里的OrderService,它只持有PaymentService接口,IDEA生成的图上只会画出OrderService依赖PaymentService,不会自动画出CardPayment和WalletPayment。要查看这两个实现类,需要手工在图上添加它们,或者右键PaymentService查看实现类。这不是IDEA画错了,而是源码在编译期能确定的关系只有这些。遇到这种场景,我的处理办法是:把运行时真正会出现的实现类手动“Add to Diagram”,再补充注释,避免看图的人误以为它们不相关。
5.2 坑二:图越大越乱,最后谁都看不清
大项目里生成整包或整个模块的图,往往出现几十个方框挤在一起。这时候最忌讳硬调整每个框的位置,浪费时间不说,下次刷新又乱了。
我的处理顺序是:
- 先隐藏所有getter、setter和构造器。
- 再用“只看邻居”的方式把图缩小到二十个类以内。
- 如果图还是太大,就按子包拆成多张小图,大图的功能只用于定位问题,小图才用于理解和沟通。
记住一个原则:大图给人找方向,小图让人看重点。想用一张图表达所有东西的人,最终什么都表达不了。
5.3 坑三:文本式UML图里的中文变成方块
文本式UML插件默认使用的渲染字体可能不包含中文字形,所以脚本里的中文注释、参与者名称会变成一排方框。
解决办法通常是两步:在插件设置里把默认字体改成支持中文的字体;如果还在用导出图片,需要同时把字体渲染选项设置好。改完之后重开一次预览窗口,中文就能正常显示。别小看这个坑,我第一次在外包项目里遇到过,评审前全场看向方框图,非常尴尬。
5.4 坑四:换了IDEA版本,菜单和快捷键对不上
不同IDEA版本对“Diagrams”这个功能的菜单位置做过调整,有的叫图,有的叫图表,还有的把它归到分析菜单里。快捷键也会因为主题和键位方案不同而变化。
最稳妥的做法是记住右键菜单这个入口,而不是依赖快捷键。如果你在某台电脑上怎么都找不到Diagrams入口,先确认IDEA版本是否完整支持该功能,再检查插件是否被禁用。不要试图背快捷键,只要你熟悉右键菜单,所有版本都能顺着找到功能。
6. 把IDEA出图变成日常动作的实战工作流
6.1 代码评审前,花三分钟生成一张包依赖图
我现在的固定习惯是:每次提交代码前,如果这次改动涉及多个类或者多个包,我会先选中自己改动的包,生成一张包依赖图,快速扫一遍新增的依赖是不是合理。
重点看两个地方:
- 有没有新增的跨层依赖,比如service突然依赖了另一个service底层的repository。
- 有没有形成环形依赖,比如A包依赖B包,B包又依赖A包。
这些检查用代码对比工具看不出来,但包图上一眼就能定位。我在过去半年里靠这个习惯拦下了至少三次明显不该合并的依赖改动,成本也就两三天一次,每次三到五分钟。
6.2 文档里的UML图怎么放,才不会“第二天过期”
UML图最大的敌人是过期。如果文档里只放一张静态截图,代码一旦变化,图就变成“历史文物”。我现在的做法是分场景处理:
- 短期沟通草稿:直接截图贴聊天记录,没必要长期保存。
- 长期设计文档:除了放图,同时保留生成这张图所用的文本描述脚本。下次要更新,直接用脚本重新生成。
- 结构说明文档:在图片下面写清楚“这张图由IDEA从某个具体提交生成,如需最新版本请重新生成”。
这样做的好处是,看到文档的人知道这份图有生成依据,不会盲目相信一个过时截图。
6.3 让UML图真正变成代码评审和交流的基建
最后分享一个我坚持了很久的做法:每周抽十五分钟,把当前正在开发的核心模块用IDEA的Show Diagram过一遍。不看细节,只看关系线有没有意外变化。
这个动作像体检,大部分时间没什么发现,但偶尔会看到一条不该出现的依赖,或者一个原本只依赖接口的服务突然依赖了具体实现类。发现问题的感觉,比日后线上出故障再定位要轻松得多。
如果你也天天用IDEA,我建议下一次评审前先试试这个法子:什么都不用准备,打开工程,选中包,右键生成一张包图。先别急着整理布局,就看箭头方向。只要这张图上出现了你没想到过的环,它就帮你省下了至少半天的排查时间。我现在最常做的动作就是这个,成本不到五分钟,收益却比很多文档都实在。