做过Java后端的人应该都有过这种经历:项目跑了一段时间,业务方突然甩过来一个需求,“把订单数据导成PDF”,或者“结算单生成一个带格式的附件”,然后你打开搜索引擎输入“itextpdf paragraph使用”,结果发现教程七零八落,版本还五花八门。实际上,itextpdf里Paragraph这个类几乎就是PDF中一切文本块的基础。你可以把它理解成HTML里的P标签,负责把一段文字变成一个能参与文档流布局的块级元素(block-level element)。跟直接操作坐标画canvas完全不一样,Paragraph能自动参与换行、分页、对齐、缩进这些排版逻辑,所以几乎所有动态导出需求都绕不开它。
下面我会结合自己在itext5和itext7两个版本里的实际使用经验,把Paragraph的构造方式、排版参数、样式定制、常见问题一次性说透。这篇不是翻译官方文档,而是把那些文档里不会写、但真实开发中一定会碰到的坑和数据都摊开给你看。
1. 先弄清楚Paragraph在itextpdf里的定位
1.1 版本差异:itext5和itext7的类路径不是一回事
很多开发者翻教程时最容易迷糊的地方,是itext5和itext7里Paragraph的包名完全不同。itext5中你要写com.itextpdf.text.Paragraph,而itext7里则是com.itextpdf.layout.element.Paragraph。别小看这个差异,项目里如果混用了,代码可能连编译都过不了。
itext7是整个itext5的一次大重构,它把内核拆分成了kernel、io、layout等模块。Paragraph被放到了layout包,意味着它的角色更偏向“布局”,而不是单纯的数据容器。itext5里Paragraph继承自Phrase,天然带了一堆块级属性;itext7里Paragraph实现了IBlockElement接口,在Document.add时被当成块级元素统一处理。这种变化带来的直接影响是:同样的“加一段文字”逻辑,在两个版本里的写法和对API的依赖完全不同。
我的建议是:新项目直接上itext7,老项目如果已经在用itext5,不要硬迁,把知识点映射到对应版本即可。因为itext7在支持Unicode字体、渲染性能、布局稳定性上都比itext5有明显改善,尤其是中文字体的处理方式,itext7要好太多。
1.2 Paragraph的本质:文本容器兼块级布局单元
Paragraph的核心作用是维护“一段文本”,这段文本内部可以只有一个字符串,也可以由多个不同样式的Text对象组成。它同时负责“块级”行为,比如在Document渲染时会自动换行、自动分页、自动应用对齐规则。
理解这个本质,后面很多问题就顺了。比如你往Paragraph里add一个很长的字符串,它不会无限向右延伸,而是在当前页面的可用宽度内做折行;当段落高度超过页面底部可用空间,Document会把它推入下一页。这些行为是“段落”这个抽象层级带来的,不是字体或文本本身具备的。
所以在文档模型里,文本的最小编号是Text(itext5里的Chunk),而Paragraph是这些文本的“容器+排版器”。你不需要手动计算每个字应该放在哪里,Paragraph会替你处理。
1.3 新手最容易忽略的点:Paragraph不负责字体具体渲染
很多人以为设置了Paragraph里的文字,整个段落字体就统一了。其实Paragraph的字体设置是“默认值”,段落内部的每个Text都可以独立覆盖它。换句话说,Paragraph在布局中提供了一套基础样式,Text可以在这个基础上做差异化渲染。一个段落里红色加粗的关键词,就是通过Paragraph默认字体 + 单个Text特殊样式组合出来的。
这也是我见过不少人在项目里反复磨蹭的地方:给Paragraph设了字体,但局部文字颜色没生效,最后发现是内部某个Text又单独设置了字体参数,覆盖掉了外面Paragraph的默认值。所以记住一个原则:Paragraph的字体是底线,Text的字体是个性化,谁在后谁优先。
2. Paragraph的基础构造与文本添加方式
2.1 构造器的选择:直接new还是带参数
Paragraph提供了很丰富的构造方式,但真实项目里常用的就是下面这几种:
| 构造方式 | 适用场景 |
|---|---|
new Paragraph() | 先创建空段落,后面通过add方法逐步填充内容 |
new Paragraph(String text) | 直接放一段纯文本,简单快捷 |
new Paragraph(Text text) | 需要用差异化样式,比如局部加粗、局部变色 |
new Paragraph(String text, Font font) | 文本和字体都要显式指定时 |
你可能会问,空构造器有什么用?实际上这是最灵活的方式。比如你在循环里根据数据动态拼接内容,前面一部分是普通文本,后面一部分需要加粗红色,这时候先new Paragraph(),再逐步往里面添加Text对象,比一开始确定好整个段落的内容更贴合真实业务。
我自己的习惯是:如果文本内容是纯展示且没有样式区分,直接new Paragraph(string)就够了;只要出现多颜色、多字体、行内图片这类需求,就用空构造器配合add方法。
2.2 添加文本的核心API:add方法的玄机
Paragraph真正强大的是它的add方法。这个方法可以用字符串、字体、Text、图片等不同对象进行重载。最常见的组合方式有两种:
Paragraph paragraph = new Paragraph(); paragraph.add("普通文本"); Text redText = new Text("红色加粗文本"); redText.setFontColor(ColorConstants.RED); redText.setBold(); paragraph.add(redText);字符串最终会被封装成一个Text对象挂到段落内部,所以纯文本添加是“快速通道”,适合简单场景;如果需要对特定部分的宽度、字色、背景做精确控制,还是建议先用Text封装。
有一个思维转换要提一下:Paragraph内部并不是简单地把这些Text拼成一个字符串,而是维护了一个元素列表。布局时,每个Text都会参与独立的渲染计算。这样可以实现局部UV样式,但也会带来额外的布局开销。所以如果一段文字完全没有差异化样式,就尽量用普通字符串,别把一个整段文章拆成成百个小小的Text对象,否则内存和性能都会很吃亏。
2.3 中文字体配置的关键步骤
绝大多数人第一次用itextpdf做导出,遇到的最大问题就是中文变成“方块”,或者干脆不显示。原因很简单:itextpdf默认的字体Helvetica不支持中文,而Paragraph在默认情况下又不会自己去找系统中文字体。
在itext7中,通常的做法是显式创建支持中文的字体:
PdfFont chineseFont = PdfFontFactory.createFont( "STSongStd-Light", "GB-EUC-H", false ); Paragraph paragraph = new Paragraph("中文段落").setFont(chineseFont);这里的STSongStd-Light是itextpdf内置的亚洲字体集合之一,加上GB-EUC-H编码可以保证简体中文正常渲染。另一个更稳妥的方案是把操作系统中文字体文件直接注册给itextpdf:
PdfFont sysFont = PdfFontFactory.createFont( "C:/Windows/Fonts/simsun.ttc,0", PdfEncodings.IDENTITY_H, true );simsun.ttc,0后面的,0是字体索引,因为TTC字体集合里可能存在多个字号;如果你不写这个索引,某些环境下会抛出找不到字体的异常。
itext5时代的中文字体处理更痛苦,需要BaseFont配合:
BaseFont bf = BaseFont.createFont( "STSongStd-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED ); com.itextpdf.text.Font font = new com.itextpdf.text.Font(bf, 12); Paragraph paragraph = new Paragraph("中文内容", font);在itext5里如果使用内置中文字体,往往还需要引一个额外的亚洲字体包(itext-asian)到classpath里,否则运行时会报找不到字体定义。这个问题在itext7中已经友好很多,但我仍然建议在初始化阶段就把字体注册好,并把PdfFont缓存下来复用,而不是每次创建段落都重新加载一次字体文件。
3. 对齐、缩进、间距与行距这组排版参数如何配合
3.1 对齐方式:从老版常量到新版枚举
文本对齐是Paragraph最常用的排版能力之一。itext5里,对齐是通过Element.ALIGN_LEFT这类常量设置的:
// itext5 paragraph.setAlignment(Element.ALIGN_CENTER);itext7里面,对齐参数被设计成了枚举类型TextAlignment:
// itext7 paragraph.setTextAlignment(TextAlignment.CENTER);在实际项目中,左对齐、居中、右对齐、两端对齐都有用武之地。尤其是两端对齐(JUSTIFIED),在生成合同、正式公函这类文档时非常显专业,整个段落左右边界都整齐,视觉上明显不简陋。但要注意:如果段落包含换行符或者中间有超长英文单词,两端对齐的效果可能不太理想,局部会出现比较大的字符间距。自己权衡着用。
3.2 缩进参数:不止是在左边加空格
很多新手觉得段落缩进就是前面加几个空格,但itextpdf里空格在换行和排版时并不可靠,而且不同字体对空格的宽度渲染也不一样。正确做法是使用Paragraph自带的首行缩进和左右缩进:
paragraph.setFirstLineIndent(24f); paragraph.setIndentationLeft(10f); paragraph.setIndentationRight(10f);setFirstLineIndent是首行缩进,单位默认是“用户空间单位”,在绝大多数PDF渲染场景中就是“点”(point)。中文Word标准里首行缩进两个字符大约是28点,看你实际字号。如果你用的是12pt字号,首行缩进24~28pt视觉上都可接受。
setIndentationLeft和setIndentationRight则是整段整体缩进,常用于嵌套引用、说明性文字。举个例子,生成一份包含公司简介的PDF附件,公司的企业简介正文通常不缩进,而“免责声明”“注意事项”这类的次级文本往往会左右同时缩进一块,视觉上形成层级对比。
这里有一个容易踩的坑:缩进值过小或为负,会导致段落边距超出页面边界。itextpdf并不会拦截这种事,它会按数值直接布局,结果就是文本被排到页面可打印区域外面,看起来像是“内容丢失”。所以当你发现导出的PDF文字莫名被裁断,先检查是不是缩进设置出了问题。
3.3 行距:固定行距和倍数行距怎么选
行距控制是Paragraph另一个高频配置点。itext里术语叫Leading,概念可以参考Word里的“行距”。itext7中明确区分了setFixedLeading和setMultipliedLeading两种模式。
setFixedLeading是固定行距,意思是不管文字多大,每行之间的垂直距离都是固定值:
paragraph.setFixedLeading(20f);用固定行距时,如果行距值小于字体实际高度,文字行之间会重叠,拥挤感非常明显。一般固定值设置在字体大小的1.2~1.5倍之间比较稳妥。
setMultipliedLeading则是按照当前字体大小按比例放大:
paragraph.setMultipliedLeading(1.5f);这种模式更符合常见的“1.5倍行距”概念。比如字体大小是12pt,计算后实际行距就是18pt。它比固定行距更省心,因为你不需要在业务代码里根据字号动态换算像素值。我个人的经验是:动态报表推荐用倍距模式,模板类正式文档推荐用固定模式,这样排版和设计稿完全对齐。itext5中对应的API是setLeading(fixed, multiplied),使用逻辑可以互相参照。
还有一个细节值得注意:Paragraph的行距是作用在整段上的,即使某一行内部出现了一个特别大的字体(比如行内图片或特大号文字),行距只会按预设模式计算,不保证一定不被撑开。如果需要行内组件不影响整段行距,那就要借助图片缩放或独立的Text高度控制。
4. 样式定制:背景、边框、下划线与内边距
4.1 给段落加上背景色和边框
Paragraph虽然主要是文本容器,但它也支持背景色和边框。这在生成表格、高亮提示、重点信息标注时很常用。itext7里的背景色和边框是这样配置的:
Paragraph highlighted = new Paragraph("重要提示内容"); highlighted.setBackgroundColor(ColorConstants.YELLOW); highlighted.setBorder(new SolidBorder(1f));setBackgroundColor给文字后方的整块区域上色,背景范围是段落的矩形内容区域,不是文字笔画的边界。这个区别很关键。如果你想要的是“文字笔画黄色”的效果,那应该给Text对象设置背景色,而不是给Paragraph设置。
SolidBorder是最简单的单线边框,线段宽度通过构造器第一个参数指定。除了实线,还可以用DashedBorder、DottedBorder做出虚线、点线的效果。不过做PDF导出时,我极少在Paragraph上用边框,更多是在表格单元格里用。因为段落加边框之后,视觉上会有一点“文本框”的味道,只有在合同提示语、审核意见里用起来比较合适。
4.2 文字特效:下划线、删除线与颜色
Paragraph层面没有直接的下划线API,但可以通过为Text设置属性来实现。这是很多初学者的认知空白点:你可能会去Paragraph里找setUnderline(),然而它并不存在于行级接口上。正确的做法是:
Text underlineText = new Text("这是带下划线的文字"); underlineText.setUnderline(); Paragraph paragraph = new Paragraph(); paragraph.add(underlineText);如果需要对下划线颜色、线宽做更精细的控制,可以调用setUnderline(ColorConstants.BLACK, 0.5f, 0f, 0f, -1f)这类带参数的重载方法,参数分别对应颜色、线宽、线型偏移等。实际开发中用默认的setUnderline()就够用了,官方API默认值会按照当前字体基线去绘制。
删除线也是同样的思路,在Text对象上调用setStrikeThrough()即可。加粗和斜体可以直接用setBold()、setItalic(),但这些方法本质上是设置字级,内部会自动切换字体样式。如果你在自定义字体,千万记得检查这个字体是否有对应的粗体和斜体变体,否则Text对象再怎么标粗也只是普通样式的渲染。
4.3 内边距和外边距的细节差异
Paragraph还有一组padding相关的方法,比如setPaddingLeft、setPaddingRight、setPaddingTop、setPaddingBottom。背景色和边框会作用在整个“内容+内边距”的矩形区域上,所以如果你想做“背景色不能贴死文字”的效果,就一定要加上padding,否则文字边缘紧挨着边框,视觉上很丑。
外边距(margin)和段落间距(spacing)实际是同一个概念,Paragraph里一般用setSpacingBefore和setSpacingAfter控制段落前后的空隙。这一点和HTML里的margin-top/margin-bottom对位。真实项目中,两个连续段落之间如果设置了过多的setSpacingAfter,再加上下一页顶部也有默认间距,就会出现多出很多空白的情况。
我踩过的一个坑是:在循环里逐条生成段落并add到Document后,每个段落分别设置了setSpacingBefore(10f)和setSpacingAfter(10f),结果页面顶部那些剧情空白叠加格外刺眼。后来我改成统一的“段后距策略”,前一个段落定下后距,后一个段落不再单独设置前距,问题立刻消失。
5. 列表、图片与复合编排:让Paragraph真正可扩展
5.1 List与ListItem:Paragraph的兄弟结构
报表里经常需要展示无序列表或有序列表,比如“服务条款”“功能特性”。itextpdf中这类需求不是用Paragraph一层层拼,而是用List配合ListItem来实现。ListItem本质上是BlockElement,内部往往又包含一个Paragraph去承载文本渲染。
List list = new List(); list.add(new ListItem("第一项内容")); list.add(new ListItem("第二项内容")); list.setListSymbol("•");在itext7中直接向ListItem传入字符串,它默认会创建Paragraph作为内容载体。这意味着之前讲的Paragraph排版能力,在ListItem里依然有效。你甚至可以向ListItem里再添加一个带样式的Paragraph,实现多行列表项。
很多人会纠结要不要自己在Paragraph前面加“• ”符号,然后控制缩进。我建议别这么做,直接用List体系,它内部处理好转弯换行和悬挂缩进,效果比我手拼字符串稳定得多。
5.2 在段落里嵌入图片的操作方式
简历、产品说明、发票头部经常需要“文字里嵌个Logo”这种效果。itext7里Paragraph可以直接添加图片对象:
Image companyLogo = new Image(ImageDataFactory.create("logo.png")); companyLogo.scaleToFit(100f, 50f); Paragraph headerParagraph = new Paragraph(); headerParagraph.add(companyLogo); headerParagraph.add("有限公司名称");图片在这里被当做一个“内联元素”参与段落行内排版。默认情况下图片会和文字叠加在基线上,实际显示效果可能需要手动调整图片偏移。常见做法是设置setFixedPosition或通过Text的rise属性来微调垂直位置,不过这个操作在itext7里有点繁琐,我建议在图片对象上处理好缩放高度,尽量减少文字和图片错位带来的视觉问题。
itext5里的方式则是通过Chunk包装图片:
Chunk imageChunk = new Chunk(image, 0f, -10f); paragraph.add(imageChunk);Chunk构造器的第二个参数是图片宽度偏移量,第三个是高度偏移量。itext5的这组参数对初学者不那么友好,很多人在这一块试错很久。如果你还在用itext5,做“文字+图片”混排时要有一点耐心,多调几次偏移量直到视觉效果满意。
5.3 复合段落:把多个不同样式Text拼成一个逻辑块
这是Paragraph最有价值的使用场景:一个段落里不同位置的文字穿着不同的“衣服”,但它们在文档流里属于同一行、同一段。比如合同条款里的关键金额加粗、颜色变红;比如消息通知里的用户命名特殊样式、链接下划线。
Text normal = new Text("这是一段普通的说明"); Text keyword = new Text("重要关键词"); keyword.setBold(); keyword.setFontColor(ColorConstants.RED); Text hint = new Text("(点击查看详情)"); hint.setFontColor(ColorConstants.BLUE); hint.setUnderline(); Paragraph paragraph = new Paragraph(); paragraph.add(normal); paragraph.add(keyword); paragraph.add(hint);这种复合段落的好处在于,它不用你去手动算坐标,也不会因为某个局部文字较长而破坏整段的结构。itextpdf在布局阶段会把Paragraph内所有文字元素按顺序逐个排列,遇到边界自动换行,整体始终保持在同一个段落框架里。
需要注意的是,这样的复合结构比单纯字符串添加多了不少内部对象开销。如果只是“整段文字用同一字体颜色”,用new Paragraph(plainText)最快;只有在真正需要差异化时,才值得把字符串拆开包装成多个Text对象。这条原则能直接关系到性能表现。
6. 常见问题与排查技巧:这些坑我都踩过
6.1 中文变成方块的根因和解决办法
这是邮件组和社区里出现频率最高的问题。表现形式是:英文和数字正常,所有中文显示为一堆小方块或者干脆消失。根因几乎都是字体没有正确配置,或者配置的字体不支持中文字符集。
在itext7里,如果你不注册中文字体就用new Paragraph("中文"),实际上Paragraph内部拿到的还是默认字体Helvetica,它连汉字的字形都没有,自然只能渲染方块。解决步骤如下:
- 注册一个系统中文字体文件,或者使用itext内置的亚洲字体集合;
- 确保在Paragraph上通过
setFont应用该字体,或者在Document创建时就设置默认字体; - 如果字体文件路径里有中文目录,某些服务器环境会有编码问题,建议把字体文件放到项目资源目录下,用相对路径读取。
PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "GB-EUC-H", false); Paragraph paragraph = new Paragraph("中文内容").setFont(font);第三步特别容易被忽略。很多项目在本地Windows跑得好好的,部署到Linux服务器就中文乱码,多半是字体文件路径找不到,或者系统里根本没有对应字体。通用做法是直接把中文字体文件打jar包里,通过InputStream注册,避免依赖服务器本地字体环境。
6.2 段落被硬生生切断:分页与跨页处理
长段落跨越页面边界时,itextpdf默认行为是直接从页尾截断,然后剩余部分从下一页继续。这个行为有时候可以接受,但更多时候阅读体验很差,尤其是一个表格内的整块说明文字被切成两半。
itext里提供了一组专门的“分页行为控制”属性:
paragraph.setKeepTogether(true); paragraph.setKeepWithNext(true);setKeepTogether(true)表示尽可能保持整个段落完整,不会在段落中间分页;如果当前页放不下整个段落,就会整体挪到下一页。setKeepWithNext(true)则让这个段落与下一个段落保持在同一页面,这个在某些合同场景里特别有用,可以避免“标题”留在页尾而“正文”跑到下一页的尴尬情况。
这两个属性并不是100%保证,如果段落本身比一整页还长,仍然会被切断,但从工程角度看已经能覆盖绝大多数业务。我一般在生成动态说明类内容时,对所有Paragraph统一设置setKeepTogether(true),效果立竿见影。
要精确控制段落是否溢出,还可以借助ParagraphRenderer去拿布局结果,但这套机制对新手过于复杂。我建议先用简单的属性门控,等确实遇到更极端的场景再深入渲染层。
6.3 性能与内存上升的排查方向
长时间批量生成大量PDF时,Paragraph本身不是性能瓶颈,常见的性能隐患往往出在周边设计上:
- 重复加载字体文件:每次创建Paragraph时都重新
createFont,不仅耗时,还会占内存。字体对象应当全局缓存,整个应用只创建一次。 - 大量细粒度Text对象:一个长文章如果拆成几万个Text,渲染和布局开销会非常可观。合理做法是尽量合并为完整字符串,只在关键位置用Text。
- 图片未做缩放:往Paragraph里塞图片时,如果不调用
scaleToFit或按目标尺寸缩放,高分辨率大图会直接撑爆PDF体积,同时降低渲染速度。 - 段落尺寸无谓重复渲染:如果可以在Document层面复用模板属性,就先用空Paragraph设置好通用样式,再复制或构造新段落,避免每个段落重复设置大量绝对值。
我在一个批量生成几千份报价单的项目里,曾把字体加载从每次创建改成静态单例,耗时直接下降了一个数量级。那个项目生成1500份PDF从原先的将近10分钟降到2分钟以内。所以如果你正在开发批量导出功能,字体复用绝对是首要优化点。
还有一个隐藏问题是itext版本不同带来的内存模型不同,itext7对PdfFont和Page对象的释放依赖比较重。如果你发现内存持续上涨,多半是没做PdfWriter的显式close,或者维护了一堆本可以被GC的Paragraph引用。该释放的对象要及时释放,避免并发场景下堆内积压。
7. 不同文本密度场景下的Paragraph实际策略
7.1 固定模板与动态数据流的处理差别
在固定模板场景(如合同、证书、发票)里,Paragraph的用法通常是“预定义好格式,再填充数据”。我的做法是维护一个模板方法,在方法内部构建统一的基础Paragraph对象:
private Paragraph baseParagraph() { Paragraph p = new Paragraph(); p.setFont(chineseFont); p.setFontSize(12f); p.setMultipliedLeading(1.5f); p.setSpacingAfter(6f); return p; }然后根据不同业务字段往里添加内容。这个“基座Paragraph”思路非常省事,所有段落的默认风格都集中在同一个地方。
在动态数据流场景(如日志导出、用户生成内容的报表)里,Paragraph更像一个“随时生长的容器”。这个时候不要过度预定义样式,因为不同数据段落的长度和结构都不一样。先创建空Paragraph,再按数据顺序添加Text,最后根据内容长度或数据属性设对齐和间距。这种流式操作是Paragraph设计的初衷,代码也更好维护。
7.2 动态行高与内容测量的经验
Paragraph在渲染时有一个“布局”过程。itext7中,你可以利用ParagraphRenderer提前完成一段文本的尺寸测量,但更简单的做法是先估算一下内容长度:
float estimatedHeight = paragraph.getRenderer() .getCurrentArea().getBBox().getHeight();这个用法依赖渲染器已经在Document上下文中运行,如果你在添加Paragraph之前就调用,很可能拿到的还是默认值。因此我建议除非有特别精确的高度控制需求,否则不要过度依赖测量值。让Document的自动布局去处理段落位置是最稳妥的,因为Paragraph内部对边界、缩进、换行的计算非常成熟,硬编码反而容易出问题。
7.3 一个模板化输出的参考思路
实际做项目时,我常用一个简单的“模板段落构建器”来减少重复代码。比如把合同正文拆成段落数组,每个元素记录文本、字体大小、对齐方式、是否加粗,然后用统一方法逐段构建并加入Document:
List<TextBlock> blocks = prepareBlocks(); Document document = initDocument(); for (TextBlock block : blocks) { Paragraph paragraph = new Paragraph(); paragraph.setFont(sharedChineseFont); paragraph.setFontSize(block.getFontSize()); paragraph.setTextAlignment(block.getAlignment()); paragraph.setSpacingAfter(block.getSpacingAfter()); document.add(paragraph); }这个方法既保持了数据与表现分离,又避免了每个段落写一遍相似代码。如果你的业务系统需要对接多种PDF模板,把内容结构抽象成“块”,再交给Paragraph去渲染,比直接在业务逻辑里大量new Paragraph要清晰很多。遇到规则复杂的情况,甚至可以再封装一层“段落工厂”,但还是那句话,别过度设计,能把样式集中在基座Paragraph上,你已经比大部分人做得专业了。
我在实际使用中的体会是,Paragraph的API并不难,难的是对它“块级布局”角色的理解。很多排版问题不是代码写得不对,而是没有按照Paragraph的脾气来写。摸清楚字体、缩进、行距、分页这几件事,你的PDF导出功能基本就赢了一半。下次再有人问itextpdf paragraph使用,你可以直接把这篇文章甩给他,至少能帮他少踩几个我当年踩过的坑。