
Presto现在更常用的名字是Trino但很多线上环境还是老版本PrestoSQL有个让人又爱又恨的点内置函数够用但一碰到业务自定义逻辑就抓瞎。我自己在数据平台部门做过几轮UDF开发从最开始的字符串清洗、JSON解析到后面的聚合函数、窗口函数踩过的坑不少。这篇就专门聊聊Presto UDF怎么从零开始写、怎么调试、怎么上线以及那些文档里不会明说但你必须知道的细节。先说清楚适用人群和场景。如果你所在的团队正在用Presto做即席查询或ETL业务方经常提类似“这个字段要按公司内部规则解析”“这段逻辑要加个置信度打分”这类需求而SQL里那些函数拼不出来那这篇就是给你准备的。默认你熟悉Java和基本SQL语法对Presto的执行模型有模糊概念即可。老版本和新版本的API略有差异但主体思路一致。这类开发的本质是把你脑子里的那段业务逻辑“翻译”成Presto执行引擎能识别的函数形式。翻译得好不好直接关系到查询能不能跑完、跑多快、会不会把Coordinator压垮。所以别把它当成简单的写Java类机制得懂一点。1. Presto UDF开发前必须想清楚的事1.1 先判断是不是真的需要UDF这是我在实际项目里最先建议团队做的一件事能不用UDF就别用。原因很现实Presto作为分布式引擎UDF是打包进Worker进程里的一旦函数有Bug或者性能有问题影响的不是单条查询而是整个集群的稳定性。很多场景用SQL自带的函数组合就能解决只是写法绕一点。举个例子之前有个需求要把一个字符串按固定长度切分成数组再展开成多行。团队里有人直接写了一个Java UDF其实一行SQL就能搞定SELECT regexp_extract_all(abc123def456, .{3})再比如新版Presto或Trino里其实已经内置了很多Lambda函数像transform、filter、reduce配合数组和Map类型能覆盖非常多的复杂逻辑。所以判断标准很简单纯数据转换、过滤、分组逻辑优先用内置函数和Lambda表达式组合涉及外部数据源访问、加解密算法、复杂概率计算、或者需要维护状态的状态机逻辑才考虑UDF如果同一个转换逻辑要被几十个查询复用UDF能显著简化SQL才值得做。我见过最离谱的项目是把一个简单的日期格式化逻辑写成UDF结果函数内部用了SimpleDateFormat在高并发下出现线程安全问题整条查询链路直接挂掉。杀鸡用了牛刀还捅了自己。1.2 确认Presto版本和对应APIPresto的发展过程中UDF相关的API在0.x版本、Trino版本之间有过几次调整。尤其是注解包路径和函数注册方式不同版本差异很大。你在动手前必须确认线上集群的精确版本号而且最好用与线上版本一致的Presto源码或SDK来编译否则可能出现类冲突或者注解扫描不到的问题。我通常这样确认环境登录Presto Coordinator节点执行SELECT version();获取版本号查看安装目录下的plugin目录是否存在已有的UDF插件确认加载方式打开presto-install/lib目录注意里面presto-spi的版本号。不同版本的核心差异主要在这几个地方版本范围注解包路径函数注册方式状态存储APIPresto 0.1xx ~ 0.2xxcom.facebook.presto.spi.function通过Plugin类getFunctions()返回旧版使用BlockBuilder需手动管理Trino 3xx ~ 4xxio.trino.spi.function同样通过Plugin类返回新版类型系统增强支持更细粒度的类型参数如果你发现线上还是老版本别急着用最新版教程去套。我踩过一次坑在Trino 360上写好的UDF放到Presto 0.227上编译直接报错原因就是注解类路径从com.facebook.presto.spi.function变成了io.trino.spi.function。后来我学乖了开发前第一件事是拉对应版本的presto-spi依赖。1.3 开发环境搭建的推荐配置UDF开发本质上是写一个Maven项目然后把编译好的插件目录丢到Presto的plugin目录下。环境搭建不复杂但有几个关键点直接决定你后面省不省心。Maven依赖配置核心部分长这样dependency groupIdio.airlift/groupId artifactIdslice/artifactId version0.38/version /dependency dependency groupIdio.airlift/groupId artifactIdunits/artifactId version0.5/version /dependency !-- 注意presto-spi的依赖要用provided scope -- dependency groupIdcom.facebook.presto/groupId artifactIdpresto-spi/artifactId version0.227/version scopeprovided/scope /dependency dependency groupIdcom.facebook.presto/groupId artifactIdpresto-main/artifactId version0.227/version scopeprovided/scope /dependencypresto-spi和presto-main必须用provided作用域因为Presto运行时自己带了这些库。如果打成fat jar把presto-spi也打进去轻则类加载冲突重则直接起不来Worker进程。另外建议引入一个打包插件把依赖也打进去但排除掉spi相关包plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.2.4/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration artifactSet excludes excludecom.facebook.presto:*/exclude excludeio.airlift:*/exclude /excludes /artifactSet /configuration /execution /executions /plugin开发时我习惯在IDEA里配置Presto的单机模式作为本地调试环境跑一个测试类模拟SQL查询来验证UDF输出。后面会专门讲怎么调试。2. 最基础的UDF写法从函数类到注册加载2.1 写一个最简单的字符串函数先从一个朴素的需求开始把字符串里所有数字提取出来拼成一个新字符串。比如输入abc123def456输出123456。这个函数逻辑简单但足够展示UDF的核心写法。首先是定义函数类package com.example.presto.udf; import com.facebook.presto.spi.function.Description; import com.facebook.presto.spi.function.ScalarFunction; import com.facebook.presto.spi.function.SqlType; import com.facebook.presto.common.type.StandardTypes; import io.airlift.slice.Slice; import io.airlift.slice.Slices; public class ExtractDigitsFunction { ScalarFunction(extract_digits) Description(提取字符串中的所有数字并拼接) SqlType(StandardTypes.VARCHAR) public static Slice extractDigits(SqlType(StandardTypes.VARCHAR) Slice input) { if (input null) { return null; } String inputStr input.toStringUtf8(); StringBuilder sb new StringBuilder(); for (char c : inputStr.toCharArray()) { if (Character.isDigit(c)) { sb.append(c); } } return Slices.utf8Slice(sb.toString()); } }注意几个关键点函数类本身是一个普通的Java类方法必须是public staticScalarFunction(extract_digits)定义了在SQL里调用时的函数名注意是下划线风格Presto SQL里函数名不区分大小写但建议统一用小写加下划线SqlType注解声明返回类型和参数类型VARCHAR类型在Java层面对应的是Slice而不是String这是个新手必踩的坑参数允许为null函数体内必须做null判断否则空值输入会直接引发NPE。2.2 Plugin类与函数注册光有函数类还不够Presto通过Java的SPI机制加载插件。需要实现Plugin接口在getFunctions()方法里返回函数类列表package com.example.presto.udf; import com.facebook.presto.spi.Plugin; import com.google.common.collect.ImmutableList; import java.util.List; public class UdfPlugin implements Plugin { Override public ListClass? getFunctions() { return ImmutableList.of( ExtractDigitsFunction.class, // 后续的函数类都在这里注册 ); } }然后需要在src/main/resources/META-INF/services/com.facebook.presto.spi.Plugin文件中写入插件类的全限定名com.example.presto.udf.UdfPlugin这个文件是Java SPI的固定机制漏了它Presto完全识别不到你的插件。我就犯过这种低级错误当时排查半天一度以为是依赖冲突。2.3 编译打包与集群部署完成上述代码后执行打包mvn clean package -DskipTests打包完成后把target目录下生成的jar文件以及通过shade插件合并的依赖jar复制到Presto安装目录的plugin/udf-plugin/目录下。注意插件目录名可以随便起但最好按业务命名比如plugin/mycompany-udf/。复制完成后需要重启Presto的Coordinator和Worker进程。这里有个容易被忽视的点虽然Presto支持动态加载插件但这个“动态”是指启动时扫描plugin目录不是运行中热加载。改完UDF必须滚动重启节点而且先重启Worker再重启Coordinator防止查询任务分发到还没加载新函数的Worker上导致报错。部署完成后通过如下SQL验证函数加载SHOW FUNCTIONS LIKE extract_digits;能看到输出就说明注册成功了。然后跑一条真实查询SELECT extract_digits(abc123def456);正常输出为123456到这一步基础链路已经通了。3. 复杂数据类型与多参数UDF3.1 数组和Map类型参数的处理真实业务中参数基本不会只是简单的字符串和数字。处理Array和Map类型需要借助Block。Block是Presto底层存储列数据的内存结构你可以把它理解为“一列数据”的封装。举个例子写一个函数输入一个整数数组返回最大值和最小值之差。核心代码如下ScalarFunction(array_range) Description(返回数组最大值与最小值的差) SqlType(StandardTypes.BIGINT) public static long arrayRange( SqlType(array(bigint)) Block block) { if (block null) { return 0; } long max Long.MIN_VALUE; long min Long.MAX_VALUE; for (int i 0; i block.getPositionCount(); i) { if (block.isNull(i)) { continue; } long value block.getLong(i, 0); max Math.max(max, value); min Math.min(min, value); } return max - min; }这里有几个容易出错的地方block.getLong(i, 0)的第二个参数是字段偏移量对于单字段类型传0即可必须判断block.isNull(i)数组里可能包含NULL值不判断的话底层会返回默认值或抛出异常如果用SqlType(array(bigint))这种写法要确保函数类里对应的SqlType注解字符串和这里完全一致包括大小写和空格否则类型匹配不上。3.2 多参数与可变参数Presto的UDF支持多个参数样式和Java方法参数一一对应。比如写一个带两个参数的字符串查找替换函数ScalarFunction(replace_all_chars) Description(将字符串中所有匹配oldChar的字符替换为newChar) SqlType(StandardTypes.VARCHAR) public static Slice replaceAllChars( SqlType(StandardTypes.VARCHAR) Slice input, SqlType(StandardTypes.VARCHAR) Slice oldChar, SqlType(StandardTypes.VARCHAR) Slice newChar) { if (input null || oldChar null || newChar null) { return null; } String inputStr input.toStringUtf8(); String oldStr oldChar.toStringUtf8(); String newStr newChar.toStringUtf8(); return Slices.utf8Slice(inputStr.replace(oldStr, newStr)); }如果业务需要支持任意个数的参数就需要用到SqlType里的array类型变通处理把可变参数封装成数组传入。Presto原生不支持类似Java的(String... args)可变参数语法所以你必须换个思路让调用方传一个数组。SELECT my_concat_all(ARRAY[a, b, c]);函数内部遍历Block即可。这个“不直接支持可变参数”的限制很多从常规Java开发转过来的同事会踩坑以为Object...能直接用结果编译期就报错。3.3 返回复杂类型如何返回数组和Map返回数组比返回标量复杂一点。标准做法是使用BlockBuilder。继续以提取字符串中所有数字和字母为例返回一个包含两个元素的数组第一个是所有数字组成的字符串第二个是所有字母组成的字符串。ScalarFunction(split_digits_letters) Description(将字符串拆分为数字部分和字母部分) SqlType(array(varchar)) public static Block splitDigitsAndLetters( SqlType(StandardTypes.VARCHAR) Slice input, BlockBuilder outArrayBuilder) { if (input null) { return null; } String inputStr input.toStringUtf8(); StringBuilder digits new StringBuilder(); StringBuilder letters new StringBuilder(); for (char c : inputStr.toCharArray()) { if (Character.isDigit(c)) { digits.append(c); } else if (Character.isLetter(c)) { letters.append(c); } } BlockBuilder varcharBuilder outArrayBuilder.beginBlockEntry(); varcharBuilder.writeBytes(Slices.utf8Slice(digits.toString()), 0, digits.length()); varcharBuilder.closeEntry(); varcharBuilder.writeBytes(Slices.utf8Slice(letters.toString()), 0, letters.length()); varcharBuilder.closeEntry(); outArrayBuilder.closeEntry(); return outArrayBuilder.build(); }这里有个重要的机制Presto的UDF返回复杂类型时框架会把你声明的返回类型的BlockBuilder通过参数传进来注意这个参数不在SQL参数列表里但必须写在方法参数末尾你往里写数据最后返回这个builder构建的Block。函数签名里这个额外的BlockBuilder参数看起来很像Java里的Out参数但它实际上是用来构建返回值的不是业务参数。这个机制不跑通一遍真的很容易绕晕。4. 规模化开发聚合函数与状态管理4.1 聚合函数的执行机制标量函数处理的是“一行进一行出”但聚合函数是“多行进一行出”涉及分布式聚合的状态合并。Presto聚合函数的核心思想是每个Worker节点做本地聚合产生中间状态然后Coordinator把中间状态合并成最终结果。因此你需要定义状态类并实现四个关键方法输入、合并、输出、获取序列化状态。我开发过一个业务场景计算一批交易金额的中位数。Presto内置没有median函数网上很多方案用percentile_approx近似但业务要求精确中位数。聚合函数的状态定义如下public class MedianState { private ListLong values new ArrayList(); public void add(long value) { values.add(value); } public ListLong getValues() { return values; } public void merge(MedianState other) { values.addAll(other.getValues()); } }关键来了状态类在分布式环境下会被序列化传输。所以MedianState必须实现Serializable接口。如果不实现本地聚合没问题但一到跨节点合并阶段就会直接抛NotSerializableException。4.2 完整实现精确中位数聚合函数函数主体如下public class MedianAggregation { AggregationFunction(median) Description(计算bigint列的精确中位数) public static void input(MedianState state, SqlType(StandardTypes.BIGINT) long value) { state.add(value); } AggregationFunction(median) public static void combine(MedianState state, MedianState otherState) { state.merge(otherState); } AggregationFunction(median) public static void output(MedianState state, BlockBuilder out) { ListLong values state.getValues(); if (values.isEmpty()) { out.appendNull(); return; } Collections.sort(values); int n values.size(); long median; if (n % 2 1) { median values.get(n / 2); } else { median (values.get(n / 2 - 1) values.get(n / 2)) / 2; } out.writeLong(median).closeEntry(); } }这里有个隐藏细节input方法中如果value为NULL怎么办严谨做法是在SQL层用WHERE过滤掉NULL值或者在函数里判断isNull否则NULL会被当成某值参与排序。我在first版实现时就没过滤结果遇到包含null的列整个中位数直接偏掉。另一个细节是状态类型必须配合StateFactory和StateSerializer注册。Presto SPI中对聚合状态的处理有几种方式最简单的是用注解AggregationFunction配合InputFunction、CombineFunction、OutputFunction但状态类本身还要实现SliceTranscoder之类的接口或者在Plugin的getStateFactories()和getStateSerializers()中注册。不同版本差异很大写之前务必看对应版本的示例代码。4.3 聚合函数的内存与性能陷阱这是聚合函数开发里最坑的地方没有之一。上述中位数实现用ListLong存储所有原始值数据量小还好几百万行数据的话光这个List就可能占用几百MB内存直接把Worker的堆顶爆。Presto的Worker通常单个查询的内存有上限聚合状态超出限制会触发查询失败。所以更稳妥的做法是在本地聚合阶段对状态做初步压缩例如使用LongArrayList代替ArrayListLong减少装箱开销如果业务允许直接用分位数近似算法比如基于Histogram的percentile_approx实现内存占用可以降到极低如果必须精确中位数考虑服务端二分优化把大状态拆分到多个阶段但UDF实践里很少这么干通常直接用SQL层做窗口函数配合排序实现。我后来在项目里遇到一次线上事故就是聚合UDF吃内存过大导致Worker频繁GC整个集群查询响应变慢。排查后发现是状态里存了太多原始值。从那以后我养成了习惯聚合函数写完先跑一个千万级数据的压力测试观察内存和GC再决定能不能上线。5. UDF的调试、测试与性能优化5.1 本地搭建Presto单机调试环境UDF开发最痛苦的是调一次要重启整个集群。我自己的做法是在本地起一个单节点Presto直接加载刚编译的插件目录。配置方法下载对应版本的Presto发行包解压后修改etc/config.propertiescoordinatortrue node-scheduler.include-coordinatortrue http-server.http.port8080 query.max-memory4GB query.max-memory-per-node2GB discovery.urihttp://localhost:8080然后修改etc/catalog/下创建一个内存数据源的配置connector.namememory这个内存连接器可以直接在SQL里使用不需要真实数据库。起服务后用presto-cli连接执行SELECT extract_digits(abc123);实测下来这种方法可以在5分钟内完成对函数逻辑的验证。如果连不上先看log/server.log大部分情况是插件加载失败或端口冲突。另外我强烈建议用单元测试配合本地调用。因为UDF方法本质上是静态方法你可以写一个普通JUnit测试直接构造Slice、Block等对象调用它public class ExtractDigitsFunctionTest { Test public void testExtractDigits() { Slice result ExtractDigitsFunction.extractDigits(Slices.utf8Slice(abc123def456)); assertEquals(123456, result.toStringUtf8()); } }这种方式跑得快适合在开发阶段频繁验证核心逻辑。等到逻辑稳定了再丢到本地Presto实例跑一次集成测试。两条腿走路效率和安全性都能兼顾。5.2 UDF性能优化避免在函数里做重活UDF的执行是逐行调用的某一个函数的性能问题会被放大到整个数据集。因此函数内部的实现必须“轻”。这里有几条我总结的硬性规则禁止在函数内部做数据库查询、远程调用。UDF是每行执行一次你如果在里面调一个HTTP接口几百万行数据就会有几百万次网络请求直接拖垮整个集群甚至拖垮下游服务。之前见过一个团队在UDF里查Redis做用户画像匹配上线当天线上集群查询全部超时。避免创建大对象。比如在函数里new一个很大的数组、List或者Map逐行创建会带来巨大的GC压力。尽量用基本类型和预分配好的容器。字符串操作用Slice不要频繁转String。Slice是Presto自己实现的零拷贝字符串结构比直接转String快很多。如果是纯字符判断逻辑可以逐字节处理Slice避免toStringUtf8()的复制开销。注意线程安全。Presto会并发执行同一个UDF所以函数里的静态变量、全局变量必须线程安全或者根本不要用。SimpleDateFormat这种非线程安全的类千万不要定义为静态成员变量。还有一点如果你的UDF逻辑复杂但输入数据量又特别大记得在SQL查询层面做数据裁剪尽量在子查询里先过滤掉不符合条件的行再进UDF处理。UDF不是银弹它只负责把一行数据变聪明没办法帮整个查询变快。5.3 用EXPLAIN分析和定位UDF瓶颈当线上出现慢查询时我一般先用EXPLAIN看执行计划。UDF相关的慢查询通常执行计划里会显示一个ScanProject节点或者Aggregation节点耗时极高具体表现是某个算子的wallTime占比特别大。如果函数的输入字段比较大比如长文本可以考虑在查询里用substr提前裁剪如果函数本身是CPU密集那就只能从算法层面优化。另外Presto的Web UI提供了详细的Query页面能看到每个Pipeline的耗时以及Operator的输入输出行数和CPU占用。这些数据对定位UDF性能问题非常有帮助。不要只看查询有没有跑完要习惯看火焰图或者时间线分布。6. 常见报错与线上问题的排查速查6.1 函数注册成功了但查询报Function not registered这类情况通常不是没注册而是你调用的函数名和注册的对不上。注意大小写、下划线、Schema前缀。如果你在Presto里配置了connector的catalog和schema有时候需要写成myudf.extract_digits(...)的形式尤其是在同一查询中跨Catalog使用时。另外确认一下当前的查询会话是不是连到了同一个目录配置如果你本机连的开发环境是A集群但函数的jar包部署在B集群自然查不到。6.2 类型不匹配UDF参数类型与调用类型不一致Presto对类型要求非常严格。函数声明参数是BIGINT但SQL里传的是INTEGER会直接报类型错误。解决办法在SQL里显式转换my_udf(CAST(col AS BIGINT))在UDF里增加重载方法同时支持INTEGER和BIGINT。重载方式很简单在同一个ScalarFunction注解下写多个方法签名即可框架会根据参数类型自动选择对应的Java方法。ScalarFunction(my_func) SqlType(StandardTypes.BIGINT) public static long myFunc(SqlType(StandardTypes.INTEGER) long input) { ... } ScalarFunction(my_func) SqlType(StandardTypes.BIGINT) public static long myFunc(SqlType(StandardTypes.BIGINT) long input) { ... }6.3 类冲突NoClassDefFoundError与依赖冲突这是部署阶段最常遇到的坑。表现是Presto启动加载插件时报告NoClassDefFoundError或者你明明在maven依赖里加了某个库但运行时找不到。原因多半是shade打包时没有排除掉Presto自带的依赖。解决方案把presto-spi、presto-main、io.airlift.slice、io.airlift.units全部设为provided用maven-shade-plugin时通过excludes把上述包排除如果UDF依赖了第三方库比如Guava而Presto内部也有Guava版本不一致时优先使用shade的relocation功能改包名。relocation patterncom.google.common/pattern shadedPatterncom.example.shaded.com.google.common/shadedPattern /relocation我第一次做shade重定位时改完包名后发现反射相关的类找不到后来才明白META-INF/services里的SPI文件也要跟着重新定位。如果排查不出来直接在本地Presto目录下解压你的插件jar看看是否存在两个相同的类文件这种问题多半是打包时没过滤干净。6.4 聚合函数结果不对多为状态合并没写对聚合函数输出错误通常是combine方法没有完整合并状态。比如你只把其他状态的值加了进来但丢失了部分的中间统计量。如果是复杂状态建议在combine方法里完整复制对方的所有字段而不是只拷贝主键。另外一个隐蔽问题output方法执行多次。Presto在某些执行策略下会多次调用output生成中间结果如果你的output里有副作用比如修改状态就会导致结果不稳定。output方法应该只读状态、写入输出绝不去改状态里的任何值。6.5 内存溢出聚合状态无限膨胀前面已经详细说了List累积导致的内存问题这里再补充一个处理思路如果业务确实需要精确聚合计算但数据量又大到单机扛不住可以把聚合函数设计成“两步聚合”配合SQL层处理或者用近似算法。在数据平台这个领域“精确但内存爆掉”和“近似但稳定”的取舍很多时候要选后者。7. UDF之外维护与治理的一些心得7.1 给UDF加版本号和灰度策略上线UDF不是写完丢上去就完事。我习惯在函数名或注册信息里带版本标识比如第一次上线的函数叫extract_digits_v1内部逻辑调整后新增extract_digits_v2而不是直接覆盖原逻辑。这样即使新版本有Bug业务方还能临时改SQL切回旧版本而不是等你重新打包部署。Presto的插件没有原生的灰度机制但你可以通过控制不同版本函数的注册来变相实现。这个方法土但非常有效。7.2 建立函数文档和输入输出规范UDF多了之后最大的问题不是开发而是维护。最简单的维护方式是每个函数类顶部写上业务含义入参类型和范围返回类型典型用法示例已知限制。我在团队里专门建了一个函数清单文档每次新增UDF都更新并且把函数注册在哪个插件目录、对应Presto版本都写在注释里这样半年后再回来维护能节约大量排查时间。7.3 权限管理UDF的可见性控制Presto本身没有很细粒度的函数权限控制一般通过系统级权限或目录权限来间接管理。如果你的平台有多团队共用建议在命名上做团队前缀隔离比如finance_、ads_开头的函数从源头减少命名冲突。如果平台有统一的查询网关可以在网关层对函数名做白名单限制这比在Presto内部折腾要省事得多。另外涉及敏感数据的UDF比如解密、脱敏函数一定要加到审计列表里定期检查调用情况。技术上UDF能做很多事但安全边界必须清晰。7.4 上线前必须做的三件检查经验积累下来每次部署UDF到生产环境之前我会强制自己过一遍这三项检查依赖检查打包产物里有没有包含presto-spi相关类有没有多个版本的冲突类压力检查用千万级数据跑一遍观察内存、GC、CPU确认不会压垮Worker回滚检查确认旧版本的jar包还有备份插件目录能否快速回滚到上一个版本。这三项听起来普通但恰恰是它们帮我避免了好几次线上事故。UDF是一种“看起来简单但上线影响面巨大”的组件必须用对待核心服务的态度去对待它。在实际开发中我个人的体感是UDF本身不难难的是对整个Presto执行框架的理解以及对自己代码在分布式环境下会不会出问题的预判。很多人写标量函数很顺手一到聚合函数就犯迷糊那是缺少对“状态”这个概念的本质理解。状态就是分布式计算里被反复序列化、传输、合并的数据结构你把它想清楚了聚合函数也就通了。最后再分享一个小心得遇到拿不准的函数行为先在本地Presto单机实例里用EXPLAIN加上少量数据把结果跑出来再决定要不要继续写。很多时候你会发现某个需求根本不需要UDF内置函数的组合就够了。把UDF用在真正必要的地方才是这个工具最正确的用法。