如果你在一个数据库选型被“历史包袱”卡死的环境里做开发,对“XXL-Job 支持 Oracle 的改造过程及注意事项”这个题目一定不陌生。XXL-Job 这套分布式任务调度平台本身很好用,但官方默认只提供了 MySQL 建表脚本和一套面向 MySQL 方言的 Mapper SQL。真到了必须把整套调度中心架在 Oracle 上的时候,麻烦就来了。我最近在一个内部运维系统里完整做了一遍这套改造,从建表脚本转换、序列生成、Mapper XML 重写,到调度锁行为验证、连接池参数调整,踩了不少坑,也把整个改动的边界、原理、验证清单摸清楚了。
这篇内容不是讲理论,而是直接给你一条可执行的改造路径:哪些文件要动、哪些 SQL 不能照抄、Oracle 和 MySQL 在字段类型、分页、空字符串、锁事务上的差异到底怎么处理、上线前要回归哪些业务场景。无论你是在传统政企项目里做适配,还是接手了一个数据库迁库任务,只要手里是 XXL-Job,又不得不落到 Oracle 上,这篇文章都能帮你省掉至少一周的试错时间。
1. 改造前先想清楚:XXL-Job 和 Oracle 的适配边界在哪里
改造一个开源框架去适配另一种数据库,最忌讳一上来就闷头改 SQL。先搞清楚整个项目里哪些环节碰数据库、哪些不碰,才能控制改造面。
1.1 访问数据库的其实只有调度中心
XXL-Job 分为调度中心(Admin)和执行器(Executor)两部分。执行器是业务侧承载任务逻辑的进程,它并不直接操作调度数据库,任务执行完后会把执行结果通过内部通信回传给调度中心,由调度中心统一落库。所以整个系统里真正依赖 MySQL 的,只有 Admin 这个 Spring Boot 应用。
这意味着改造范围相对集中:数据库初始化脚本、MyBatis Mapper XML、配置文件、驱动依赖,翻来覆去就这四块。Java 里那些调度线程、任务触发逻辑、RPC 通信根本不用动。我见过有人一开始想重写调度逻辑去“兼容” Oracle,这是典型的过度设计,完全没必要。
1.2 改动集中在三个位置
具体落到文件级别,这次改造实际涉及的东西很少:
- 官方脚本目录下的
tables_xxl_job.sql,这是 MySQL 版建表脚本,需要整体转换成 Oracle 版。 - Mapper XML 目录里所有涉及
LIMIT、NOW()、useGeneratedKeys、字符串拼接等方言写法的 SQL。 application.properties里的数据源配置,以及pom.xml里需要引入的 Oracle JDBC 驱动。
搞清楚这个边界后,剩下的工作就是一场“持久层适配战”。建议改造前先导出一份当前使用的 XXL-Job 版本对应的 MySQL 建表脚本,逐表核对字段。
1.3 方案取舍:纯 Oracle 适配还是双库兼容
接手这类改造时,经常有人问“能不能让系统同时支持 MySQL 和 Oracle”。答案是能,但要掂量成本。双库兼容意味着所有 Mapper SQL 都要做数据库方言识别,比如引入 MyBatis 的DatabaseIdProvider,每个<select>、<insert>维护两套实现,分页、主键生成、日期函数处处是分支。维护成本高,且后续升级 XXL-Job 时每次都要同步改两套 SQL。
我这次选择的是“纯 Oracle 适配”。原因很简单:目标生产环境只有 Oracle,没有双库并存需求。如果你也是类似场景,可以坚定一点,单一数据库适配最稳,也最好维护。只有明确要求“同一套程序既部署在 MySQL 环境又部署在 Oracle 环境”时,才需要考虑双库方案。
1.4 版本与前置检查
动手之前,把版本确认放在第一位,这决定了后续 SQL 语法怎么选:
- XXL-Job 版本,不同版本的表结构有差异,比如老版本没有用户表,新版本加了
xxl_job_user,要用对应版本脚本去转换。 - Oracle 数据库版本,11g 和 19c 的分页写法差很多。很多传统环境还在 11g,所以分页最好用 ROWNUM,不要直奔 12c 的
OFFSET FETCH。 - Oracle 字符集,如果目标库是 ZHS16GBK 或 AL32UTF8,字段长度的处理完全不同,后面细说。
另外建议关注存量数据:如果线上 MySQL 里已经积累了海量调度日志,迁移时未必要把历史日志全部搬过去,配置表、任务表必须迁移,日志表可以从切换日重新累积,这样能省掉大量迁移时间和表空间规划成本。
2. 建表脚本迁移:MySQL 转 Oracle 的字段类型与序列细节
建表脚本是整个改造的基础。直接用工具一键转换生成的脚本往往能跑通一半,但细节坑很多,下面把必须人工核对的点全部拆开。
2.1 官方表结构全貌
XXL-Job 的调度库一共就那么几张表,分别是执行器管理表、任务信息表、调度日志表、日志报表表、GLUE 脚本表、执行器注册表、调度锁表,以及新版才有的用户表。每张表的主键大多采用数据库自增方式。
迁移时不要只盯着字段类型,还要看每一列在实际代码里是怎么被使用的。比如调度日志表里有大文本字段,如果用 CLOB 存储,后续在 Mapper 里做 WHERE 条件扫描就会碰到 Oracle 对 CLOB 字段比较的限制,这是后话,但建表时就要先想好方案。
2.2 字段类型映射速查表
MySQL 与 Oracle 字段类型差异很大,我整理了一张足够覆盖 XXL-Job 全表的映射表:
| MySQL 类型 | Oracle 类型 | 说明 |
|---|---|---|
bigint(20) | NUMBER(19) | 主键、日志表 ID 建议统一用这个 |
int(11) | NUMBER(10) | 状态码、序号类字段 |
tinyint(4) | NUMBER(1) | 布尔/状态标志位 |
varchar(n) | VARCHAR2(n CHAR) | 重点:建议显式写CHAR,理由见 2.4 |
datetime | DATE | Oracle 的 DATE 到秒级,调度平台够用 |
timestamp | TIMESTAMP | 需要小数秒才用,XXL-Job 中少见 |
text/mediumtext | CLOB | 日志内容、GLUE 源码等大文本 |
decimal | NUMBER(p, s) | 金额类字段,XXL-Job 里基本没有 |
tinyint(1) | NUMBER(1) | 布尔值的 Oracle 等价表达 |
这里有个容易错的点:MySQL 的datetime和timestamp都可能带DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP。Oracle 里日期默认值直接写SYSDATE即可,但ON UPDATE CURRENT_TIMESTAMP这种“修改时自动更新”的语义在 Oracle 里没有等价写法。好消息是 XXL-Job 的 Mapper SQL 里update_time基本都是代码里显式传入的,所以不补触发器问题也不大。
2.3 主键与序列:给每张关键表配一个序列
Oracle 没有 MySQL 的AUTO_INCREMENT,主键生成依赖序列(SEQUENCE)。我建议为每张有自增主键的表单独建一个序列,命名保持和表名对应,比如任务信息表对应XXL_JOB_INFO_SEQ,日志表对应XXL_JOB_LOG_SEQ。序列的起始值要注意:如果 MySQL 里的老数据要继续沿用,START WITH必须设置成当前表MAX(id)+1,否则切库后第一次插入就会主键冲突。
数据库里建序列只是第一步,真正让应用用上序列,是在 MyBatis 的 INSERT 语句里通过selectKey去取,这个放到第三章细说。这里先补充一个重要原则:不要给表写“插入触发器”来自动填充主键。触发器方案看起来很省事,但会让 SQL 执行路径变隐晦,排查问题困难,而且 XXL-Job 的实体很多地方插入后要立刻拿到回填主键,触发器和selectKey同时存在反而会行为冲突。应用层显式取序列最可控。
2.4 字符集、默认值与表注释
Oracle 的VARCHAR2(n)默认长度单位是字节,不是字符。MySQL 里varchar(255)存 255 个英文没问题,但存中文时一个汉字在 AL32UTF8 下可能占 3 个字节,如果直接转成VARCHAR2(255),写入时极易报“值太大”错误。所以字段长度一定要显式写成VARCHAR2(n CHAR),意思是按字符数计算。这是迁移脚本里最值得逐列检查的一处。
由于 Oracle 对大小写敏感,表名和列名建议统一使用大写。如果建表时不加双引号,Oracle 会默认把标识符转成大写,MyBatis 里 SQL 写小写也能正常识别,但如果你手工建表时用了双引号加小写,后面所有 SQL 都得跟着带引号,非常折磨。
MySQL 建表里的COMMENT='xxx'在 Oracle 里不能直接跟在字段后面,需要单独执行COMMENT ON TABLE和COMMENT ON COLUMN语句。注释不写不影响功能,但强烈建议保留,因为后期接手的 DBA 看不到字段含义会疯掉。
最后是这个最隐蔽的坑:Oracle 会把空字符串当成 NULL。MySQL 里''和 NULL 是两种值,Oracle 里它们是同一个东西。XXL-Job 的 Mapper 里凡是出现where xxx != ''、where xxx = ''这类条件,迁移后行为都可能变,要么查不出数据,要么结果集莫名变大,后面章节会专门讲怎么改。
3. Mapper 层改造:SQL 方言逐个击破
建表脚本转换只是地基,真正的工作量在 MyBatis Mapper XML 里。XXL-Job 的 Mapper SQL 整体不复杂,但几乎每个关键操作都用了 MySQL 方言,需要逐个改。
3.1 INSERT 与主键回填:从 useGeneratedKeys 到 selectKey
MySQL 版 Mapper 里,插入任务或日志时通常会写useGeneratedKeys="true" keyProperty="id",插入后由 MySQL 自增主键回填到 Java 实体。Oracle 没有自增,必须改成selectKey,在真正的 INSERT 执行前先从序列里取一个值:
<insert id="save" parameterType="XxlJobInfo"> <selectKey keyProperty="id" resultType="java.lang.Integer" order="BEFORE"> SELECT XXL_JOB_INFO_SEQ.NEXTVAL FROM DUAL </selectKey> INSERT INTO XXL_JOB_INFO ( ID, JOB_GROUP, JOB_DESC, ... ) VALUES ( #{id}, #{jobGroup}, #{jobDesc}, ... ) </insert>这里的resultType要和 Java 实体里id字段类型保持一致,有的表id是Long,有的表是Integer,搞错会在插入后收到类型转换异常。order="BEFORE"很关键,意思是先拿到序列值再插,不能写反。
我用这套方案改完后,原本所有依赖“插入后实体主键非空”的业务逻辑,比如插入 GLUE 日志后要用主键反查,全部无需改动。这是最小侵入的方式。
3.2 分页查询:从 LIMIT 到 ROWNUM 的通用写法
XXL-Job 的任务列表、日志列表都做了分页,MySQL 版 SQL 里是标准的LIMIT #{offSet}, #{pagesize}。Oracle 不支持这个语法。如果你的环境是 12c 及以上,可以直接用:
SELECT * FROM XXL_JOB_INFO OFFSET #{offSet} ROWS FETCH NEXT #{pagesize} ROWS ONLY但传统企业环境很多还停留在 11g,所以我建议统一采用 ROWNUM 三层分页写法:
<select id="pageList" resultMap="XxlJobInfoResultMap"> <![CDATA[ SELECT * FROM ( SELECT T.*, ROWNUM RN FROM ( SELECT * FROM XXL_JOB_INFO WHERE 1 = 1 <if test="jobGroup != null and jobGroup != ''"> AND JOB_GROUP = #{jobGroup} </if> ORDER BY ID DESC ) T WHERE ROWNUM <= #{offSet} + #{pagesize} ) WHERE RN > #{offSet} ]]> </select>三层结构的逻辑是:最内层完成真实查询和排序,中间层用ROWNUM <= 结束行号初步截断,最外层把前offSet行过滤掉。这里有个容易踩的坑:ROWNUM 是在排序之前还是之后生效,取决于它在哪个层级。如果在最外层直接写ROWNUM <= 10,拿到的可能是排序前的行,分页顺序就乱了。所以一定是先排序、再取范围。另外 XML 里<=是非法字符,记得用<![CDATA[]]>包住,或者写<=。
有些项目喜欢用ROW_NUMBER() OVER (ORDER BY ...)做分页,11g 也支持,但性能和可读性上我更推荐 ROWNUM 方案,因为 ROWNUM 能在扫描阶段提前终止,命中的数据量更少。
3.3 日期函数与字符串拼接:看着像但行为不一样
MySQL 和 Oracle 的日期函数写法差异很大。改造时搜到最多的就是NOW()。XXL-Job 的日志清理、注册表清理、报表统计 SQL 里都用了NOW(),Oracle 下要统一改成SYSDATE。比如清理过期日志:
DELETE FROM XXL_JOB_LOG WHERE TRIGGER_TIME < SYSDATE - #{logRetentionDays}注意 Oracle 里SYSDATE - 90表示 90 天前,这个算术语义和 MySQL 的DATE_SUB(NOW(), INTERVAL 90 DAY)等价,但简洁很多。
字符串拼接是另一个高频改动点。MySQL 用CONCAT函数,Oracle 也提供CONCAT,但只支持两个参数。XXL-Job 的模糊查询里有CONCAT('%', #{jobDesc}, '%'),这种写法在 Oracle 里直接会报参数个数错误。稳妥的做法是用连接符:
AND JOB_DESC LIKE '%' || #{jobDesc} || '%'还有一个坑藏在 NULL 语义里。MySQL 的CONCAT('a', NULL)返回 NULL,Oracle 的'a' || NULL返回'a',因为 Oracle 把 NULL 当作空字符串处理。这个差异一般不会让你的 SQL 报错,但会让结果集多出一些之前不会出现的数据,排查起来非常隐蔽。
日期格式化也要注意:MySQL 用DATE_FORMAT(date, '%Y-%m-%d'),Oracle 用TO_CHAR(date, 'YYYY-MM-DD');字符串转日期同理,STR_TO_DATE对应TO_DATE。报表查询里如果按天分组,尤其要检查这类写法。
3.4 大字段、批量 INSERT 与动态 SQL 的隐藏风险
日志表和 GLUE 表里都有 text 类型,迁移成 CLOB 后有个明显限制:CLOB 不能直接用于DISTINCT、GROUP BY、ORDER BY、=, 甚至部分场景的LIKE。如果你只是SELECT *把这些字段查出来,绑定到实体,没问题;但一旦有人对日志内容做模糊搜索,SQL 就会报ORA-00932: inconsistent datatypes。
遇到确实需要在大文本上做 LIKE 的场景,可以用TO_CHAR(CLOB字段)先转成字符串再匹配,但这样没法走索引,只适合低频管理端查询。XXL-Job 默认没有在日志大文本上做查询筛选,所以通常不用改。这里提出来主要是提醒别在后续自定义功能里顺手踩雷。
批量插入方面,MySQL 支持一行INSERT INTO t (...) VALUES (...), (...), (...),Oracle 11g 里要用INSERT ALL语法,或者干脆在 Java 里循环单条插入。XXL-Job 原生的业务代码基本是单条插入 SQL,所以这个问题不突出,但如果你在项目里基于 XXL-Job 二次开发过批量写入逻辑,迁移时就要重点排查。
动态 SQL 里所有<if test="xxx != null and xxx != ''">判断都是 Java 层面生效的,数据库方言不影响,可以原样保留。唯一要注意的是,Mapper XML 里如果直接查出了 Map 类型,Oracle 返回的列名默认是大写,而 MySQL 可能返回小写或原样。XXL-Job 某些版本的报表统计接口会返回List<Map<String, Object>>,当代码里用map.get("triggerDay")时,Oracle 环境可能拿到 null。遇到这种问题,要么在 SQL 里给字段起别名AS "triggerDay",要么把 Java 代码的取值 key 改成大写。
4. 调度锁与运行参数:并发场景的兼容性验证
数据库适配最怕的不是单条 SQL 报错,而是并发行为不一致。XXL-Job 的调度中心支持集群部署,靠数据库锁保证同一时间只有一个调度线程在跑,这块在 Oracle 下的表现必须单独验证。
4.1 调度锁 FOR UPDATE 在 Oracle 下的行为
XXL-Job 调度中心启动后,会有专门的调度线程不停地拿锁、触发任务、释放锁。核心逻辑是:关闭连接自动提交,然后执行
SELECT * FROM XXL_JOB_LOCK WHERE LOCK_NAME = 'schedule_lock' FOR UPDATE
拿到这行的行锁后开始做调度,完成后提交事务并关闭连接,锁随之释放。如果有第二个调度中心实例也在抢这个锁,它会在同一个查询上阻塞等待,直到第一个实例 commit。
这个机制在 Oracle 上完全成立,因为 Oracle 同样支持SELECT ... FOR UPDATE行锁,而且默认隔离级别是 READ COMMITTED,锁等待行为更干脆。MySQL 默认 REPEATABLE READ 下,多个实例抢锁时还可能因为 MVCC 快照问题产生一些微妙差异,Oracle 因隔离级别更低,反而更直接。
改造时我专门做了双实例测试:同时启动两个 Admin 指向同一个 Oracle 库,反复触发任务,确认没有出现重复调度。测试结论是:调度锁这段代码不需要改,但要注意锁表本身不需要迁移主键序列,因为它没有自增列。
有一点值得提醒:调度线程拿到锁后会触发一大批任务,这个过程中锁一直持有。如果有一个任务处理特别慢,另一个调度中心实例会一直等锁,日志里可能出现连接等待变长。这不是 Oracle 的锅,是原有设计就如此。但只要你的任务执行逻辑不在调度线程里做重活,问题不大。
4.2 事务隔离级别差异对状态流转的影响
MySQL 默认 REPEATABLE READ,Oracle 默认 READ COMMITTED。差异主要体现在同一事务内多次查询的快照行为。XXL-Job 里注册表心跳更新、日志状态流转这类逻辑都是单条 UPDATE,事务内不会依赖“同一查询在不同时间点的快照一致性”,所以迁移后行为没有变化。
有一点需要确认:调度中心的数据源事务配置是 Spring 管理的,不需要去改隔离级别。千万不要为了“对齐” MySQL 把 Oracle 隔离级别改成 SERIALIZABLE,那只会引入大量锁等待和并发性能问题。保持 Oracle 默认即可。
4.3 驱动、连接池与初始化配置
Oracle JDBC 驱动不会出现在 Maven 中央仓库的标准依赖里,内网环境更常见的方式是先拿到ojdbc8.jar,手动安装到本地仓库或私有仓库:
mvn install:install-file \ -Dfile=/path/to/ojdbc8.jar \ -DgroupId=com.oracle \ -DartifactId=ojdbc8 \ -Dversion=19.3.0.0 \ -Dpackaging=jar然后在pom.xml里引用即可。版本选择上,11g 数据库可以配 12c 或 19c 驱动,向后兼容性一般没问题。
application.properties里数据源配置改成:
spring.datasource.url=jdbc:oracle:thin:@//192.168.1.10:1521/orcl spring.datasource.username=xxl_job_admin spring.datasource.password=xxxxx spring.datasource.driver-class-name=oracle.jdbc.OracleDriver如果连接池用的是 Druid,原来的连接校验 SQL 是SELECT 1,Oracle 里要改成SELECT 1 FROM DUAL,否则启动或取连接时会周期性报错。这个配置藏在连接池参数里,很容易漏掉。HikariCP 默认不强制校验 SQL,影响小一些,但我还是建议显式配好,避免连接被网络设备掐断后无法自动恢复。
字符集层面,最常遇到的问题是中文乱码。数据库的NLS_CHARACTERSET和 JDBC 客户端的NLS_LANG不一致时,任务描述和日志中文会变成问号或乱码。通常设置应用服务器的NLS_LANG=AMERICAN_AMERICA.AL32UTF8(对应 AL32UTF8 库),或ZHS16GBK(对应 ZHS16GBK 库),和数据库保持一致即可。另外jdbc:oracle:thin连接串也可以补上?oracle.net.CONNECT_TIMEOUT=10000之类的超时参数,生产中很有用。
5. 常见问题与排查速查表
这一节是整场改造下来最值钱的部分,直接按“现象、根因、处理”整理成速查表,遇到问题可以对照着来。
5.1 典型报错与原因分析
| 错误现象 | 根因 | 处理方式 |
|---|---|---|
ORA-00942: table or view does not exist | 表名大小写不一致,或者登录 schema 不对 | 确认用户有权限访问表;统一用大写表名建表,避免带引号的小写表名 |
ORA-00904: invalid identifier | 列名识别不了,通常也是大小写问题 | 检查列名是否带双引号;Oracle 默认大写列名 |
ORA-00932: inconsistent datatypes | 对 CLOB 字段用了比较、排序、DISTINCT | 改用TO_CHAR(CLOB字段)处理,或确认是否真需要在大文本上过滤 |
ORA-01461: can bind a LONG value only for insert into LONG column | JDBC 绑定 CLOB 大字段时驱动版本和参数不匹配 | 升级 ojdbc 版本;检查 MyBatis 的 jdbcType 是否写成了 CLOB,必要时配SetBigStringTryClob这类连接参数 |
SQLSyntaxErrorException | Mapper 里还残留 MySQL 方言 | 全局搜索limit、NOW()、CONCAT(、useGeneratedKeys,逐个替换 |
ORA-01000: maximum open cursors exceeded | 连接或游标未释放,常见于连接池配置过小或分页查询绑定参数过多 | 调大maxOpenPreparedStatements;排查是否有人手动获取连接后没关闭 |
| 中文乱码 | 数据库字符集和 JDBC NLS_LANG 不一致 | 统一使用AL32UTF8或ZHS16GBK;重启应用后重新测试写入 |
| 调度中心实例等锁后超时 | 调度线程持有的连接事务长时间不提交,或连接池过小 | 检查调度锁的 finally 是否正常 commit/close;适当调大连接池上限 |
5.2 回归验证清单
改造完成后,不能只验证“能登录、能查列表”就上线。至少要把下面这些场景在 Oracle 环境完整过一遍:
- 启动调度中心,观察日志里调度线程正常启动,无 SQL 异常。
- 执行器正常注册、心跳上报,管理端能看到在线执行器。
- 新建一个简单任务,手动触发一次,确认调度日志产生且回调状态正确。
- 编辑任务描述、修改 Cron 表达式,确认带特殊字符和中文的内容能正常保存。
- 删除任务,确认关联日志不会被硬删导致外键异常。
- 查看执行日志详情,特别是包含长文本和异常堆栈的日志,确认 CLOB 读取正常。
- 查看报表统计页,按天分组的数据能正常展示。
- 双部署两个 Admin 实例,确认同一时间只有一台在调度,无重复触发。
- 配置 3 天日志保留策略,等自动清理任务跑一遍,确认 DELETE 语句正常。
- 模拟一个 Oracle 数据库连接断开的场景,确认连接池能自动恢复,调度任务不中断。
这 10 项是我每次做数据库适配都会回归的底线清单,缺一项都可能把问题带到生产。
5.3 改造中的三个避坑心得
第一,不要完全信任数据库迁移工具的自动转换结果。我用 DataGrip 和 DBeaver 都试过一键迁移,能解决表结构复制,但字段长度单位、默认值、注释、索引命名这类细节它不会管,最终还是手工逐表核对才靠谱。建议把转换后的脚本当成草稿,不要直接在生产执行。
第二,Mapper 的改动要配合全局搜索来查漏。改造完主流程后,我习惯在 Mapper 目录统一搜几个关键词:limit、NOW()、CONCAT(、useGeneratedKeys、ON DUPLICATE、REPLACE INTO。凡是搜出来的都逐条确认,能消灭九成以上的漏网之鱼。
第三,上线前准备一套回滚方案。数据库适配不是改代码这么简单,万一生产上某个低频功能在 Oracle 下表现异常,可能影响整个调度链路。我这次上线前把原 MySQL 环境整体保留了两周,所有新任务先在 Oracle 环境跑,确认稳定后才彻底切换流量。实践证明,这个回滚缓冲给了我很大的试错空间。
最后再分享一个实际遇到的坑
改造接近尾声时,我一度以为所有 SQL 都改干净了,结果 dashboard 的报表接口在 Oracle 环境下返回的数据一直不对。排查到半夜才发现,问题不在 SQL 语法,而是 MyBatis 查询返回的 Map 里,Oracle 默认列名全是大写,代码里却是用小写 key 去取值的。这类问题在 MySQL 环境下完全不存在,因为 MySQL 的列名大小写处理更宽松。后来把统计 SQL 里的字段都加了大写别名,又在 Java 端统一了取值方式才算结束。
所以我想多说一句:数据库适配里,真正危险的往往不是那些一眼就能看出差异的语法,而是两边“默认行为”不一样的地方。空字符串与 NULL 的等价、列名大小写返回、CLOB 字段的比较限制,这些都需要你用测试数据一条条验证,而不是看文档能看出来的。把上面这些坑提前踩一遍,你的改造就会顺利得多。