
写代码的人大概都经历过这个场景数据库里刚定稿一张业务表改动十几次字段你跟着改了十几遍实体类、Mapper接口、XML文件里的ResultMap和SQL。项目一多这活儿就变成纯粹的体力活枯燥不说还特别容易漏字段。到后来我干脆把这类重复工作全部交给工具核心思路就一条凡是能由约定和规则生成的代码绝不手写。MyBatis Generator以下简称MBG就是干这个的它可以根据数据库表结构把实体类、Mapper接口、XML映射文件这些“模板代码”一次性生成好并且支持自定义覆盖规则、类型转换、注释策略甚至能对接MyBatis-Plus这类增强框架。这篇文章我打算把实际项目中MBG的完整玩法梳理一遍从最基础的XML配置、三种启动方式到Example条件构造、分页缓存接入、拦截器配合再到我踩过的一些坑一次性讲透希望对正要入坑或已经在用但没深挖过的人有帮助。1. 为什么我还在用MyBatis Generator你不妨先想一个问题一个项目里真正需要你手写的SQL有多少按我自己的经验差不多七成以上是单表CRUD、条件查询、批量操作这类完全可以从表结构推导出来的固定写法。剩下的三成才是需要仔细打磨的复杂联表、聚合查询、动态SQL优化这些也确实不适合让生成器代劳。MBG解决的就是前面那七成它把“从表结构到基础SQL”这一段过程自动化让你把精力集中在业务建模和复杂查询上。有人可能会说现在有了MyBatis-Plus直接用BaseMapper就行连生成都不需要。这话对了一半。MP的BaseMapper确实内置了很多通用方法但它只解决了“运行时的单表操作”实体类你还是得一个个建而且在一些既有项目里团队代码风格、审核要求、历史封装都要求保留XML映射和自定义SQL这时候MBG生成的完整三件套实体类、Mapper接口、XML依然是落地成本最低的方案。更关键的是MBG支持生成带注释的、符合团队规范的基础代码新人接手也容易理解。还有一个很容易被忽略的点MBG不是一次性工具它可以在表结构迭代时反复执行配合覆盖策略和“增量生成”特性保持实体类和表结构同步。我见过太多人建表后手写实体类结果字段改名只改了一处运行时才发现映射对不上线上出问题。用MBG之后这种低级错误基本绝迹因为生成物和表结构始终是强一致的关系。1.1 它到底帮你省了哪些事拿最常见的订单表来举例一张order_info表如果手写你得完成这些工作建实体类把表字段翻译成Java类型和命名规范的属性加上getter/setter。写Mapper接口声明insert、updateByPrimaryKey、selectByPrimaryKey、deleteByPrimaryKey以及各种按条件查询的方法。写XML映射文件配置ResultMap把每个字段和属性的映射关系写清楚再把上述SQL的where条件、set子句逐一实现。这三件事说难不难但架不住数量多。一个中型项目动辄几十上百张表纯手写的时间成本和错误率都很可观。MBG一张表全部生成下来只需要几秒而且它生成的SQL是经过官方多年打磨的对null值处理、主键回填、批量操作用例等细节都考虑到了比大多数手写SQL稳得多。另外MBG还生成一个容易被低估的东西——Example类。它提供了一套类型安全的动态条件构造API允许你在Java代码里用链式调用的方式拼查询条件比如example.createCriteria().andStatusEqualTo(1).andCreateTimeGreaterThan(start)。这种方式比手拼字符串SQL安全得多也比到处写if标签要清爽尤其在管理后台那种查询条件特别多的场景下非常实用。1.2 什么时候用生成器什么时候手写我的判断标准比较简单表结构稳定、操作是常规CRUD的直接交给生成器表结构极其简单、总共就三五个字段的手写反而更快复杂查询、报表类SQL、需要精心调优的慢查询毫无疑问应该手写或者通过XML自定义SQL去优化。还有一类情况我也建议手写就是字段特别多的宽表。有些业务表动辄三四十个字段MBG默认会生成包含所有字段的insert、updateSQL会变得特别冗长。这时候有两种处理方式一是用insert selective这类只插入非空字段的方法二是干脆手写只涉及核心字段的SQL。我在实际项目里通常会生成全部代码但重点使用selective系列方法既能保证完整性又避免了无效字段的更新。1.3 MBG在MyBatis技术栈里的位置把MBG放到整个MyBatis技术栈里看它的角色应当很清晰它负责“静态代码生成”解决的是编码效率问题MyBatis本身负责“SQL映射与动态SQL执行”解决的是数据库访问问题MyBatis-Plus这类增强框架呢解决的是“让单表CRUD连XML都不需要写”的问题而像PageHelper分页插件、自定义拦截器这些解决的是“SQL执行过程中的横切增强”问题。这几层不是互相替代的关系而是可以叠加的。我现在的项目里就同时使用MBG和MyBatis-PlusMBG生成实体类和Mapper接口再手动把Mapper接口改成继承MP的BaseMapper同时保留XML文件用于复杂查询。这样既有MP的便捷单表操作又有XML的灵活SQL扩展两边的好处都拿到了。后面我会具体讲这个组合怎么配。2. 配置一把梭从XML到Java配置的核心细节MBG最传统的使用方式就是写一个generatorConfig.xml这个配置文件就是整套生成逻辑的“总指挥”。新手一上来最容易蒙的就是这一大坨XML不知道每个节点是干嘛的抄来抄去报错了也不知道怎么改。我先把核心节点按作用拆开讲一遍你对照着看会清楚很多。2.1 XML配置逐项拆解一个最小可用的配置文件长得是这个样子?xml version1.0 encodingUTF-8? !DOCTYPE generatorConfiguration PUBLIC -//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd generatorConfiguration context idmysqlContext targetRuntimeMyBatis3 defaultModelTypeflat jdbcConnection driverClasscom.mysql.cj.jdbc.Driver connectionURLjdbc:mysql://localhost:3306/your_db?useUnicodetrueamp;characterEncodingutf8 userIdroot passwordyour_password/ javaTypeResolver property nameforceBigDecimals valuefalse/ /javaTypeResolver javaModelGenerator targetPackagecom.example.entity targetProjectsrc/main/java property nameenableSubPackages valuetrue/ property nametrimStrings valuetrue/ /javaModelGenerator sqlMapGenerator targetPackagemapper targetProjectsrc/main/resources/ javaClientGenerator typeXMLMAPPER targetPackagecom.example.dao targetProjectsrc/main/java property nameenableSubPackages valuetrue/ /javaClientGenerator table tableNameorder_info domainObjectNameOrderInfo property nameuseActualColumnNames valuefalse/ /table /context /generatorConfiguration这里每个节点的作用我按执行顺序说明。context是整个生成任务的容器一个配置文件里可以有多个context比如一个连MySQL一个连Oracle各自独立配置互不干扰。jdbcConnection很简单干的就是连接数据库读表结构的事。这里有个容易被忽略的细节connectionURL里的符号在XML里必须写成amp;否则解析配置文件会直接报错我第一次配的时候在这里卡了半小时。javaModelGenerator负责生成实体类它的trimStrings属性值得专门说一下。置为true之后MBG会在所有String字段的setter方法里自动加一句trim()对于用户输入类数据这个特性可以提前消除首尾空格带来的坑。不过如果你处理的数据本身有保留首尾空格的需求就别开这个功能按业务来。sqlMapGenerator负责生成XML映射文件javaClientGenerator负责生成Mapper接口。注意这两个的targetProject一个指向resources目录、一个指向java目录千万别配反了。还有个关键属性叫typeXMLMAPPER表示生成传统XML接口模式如果是ANNOTATEDMAPPER则只生成注解SQL不生成XML具体用哪种取决于项目风格但绝大多数项目都会选择XMLMAPPER方便后期加自定义SQL。2.2 表结构的映射与类型转换MBG根据数据库字段类型推断Java类型这一步由javaTypeResolver控制。默认的映射规则大致是数据库的INTEGER映射到IntegerBIGINT映射到LongVARCHAR映射到StringDATE映射到DateTIMESTAMP映射到TimestampDECIMAL映射到BigDecimal等。这些默认值其实满足大部分场景但有几个特例需要额外处理。第一个是MySQL的tinyint(1)。默认情况下它会被映射成Byte可业务里tinyint(1)往往就表示布尔值如果实体字段是Byte在外面做判断时总得写status 1很不直观。遇到这种情况可以在table节点里用columnOverride强制指定类型table tableNameuser columnOverride columndeleted javaTypejava.lang.Boolean jdbcTypeTINYINT/ /table第二个是DECIMAL(10, 2)这类金额字段如果forceBigDecimals设置为false它会映射成BigDecimal这其实是好事金额字段用BigDecimal才能避免浮点精度问题。第三个就是默认的驼峰转换database的order_no自动变成orderNo这依赖useActualColumnNames为false一般保持默认就好。我建议在建表阶段就对字段命名和类型做规范约定这样生成出来的实体类会非常干净。反而如果表字段命名不统一一会儿下划线一会儿驼峰生成物就会很难看需要大量的columnOverride去修那就得不偿失了。2.3 三种运行方式对比Maven插件、IDE插件、独立Main配置文件写好之后怎么让它跑起来主要有三种方式。第一种Maven插件这是我最推荐的方式尤其适合项目已经用Maven管理的情况。先在pom里加插件配置plugin groupIdorg.mybatis.generator/groupId artifactIdmybatis-generator-maven-plugin/artifactId version1.4.2/version configuration configurationFilesrc/main/resources/generatorConfig.xml/configurationFile overwritetrue/overwrite verbosetrue/verbose /configuration dependencies dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency /dependencies /plugin然后执行mvn mybatis-generator:generate就行了。这里要注意数据库驱动依赖必须加进插件里否则会报找不到驱动类。第二种IDE插件比如IDEA的MyBatis Generator插件在编辑器里右键就能跑。优点是图形化操作直观适合不想敲命令的场景。缺点是插件版本和MBG核心版本可能不一致生成出来的代码风格会有差异多人协作时也不好统一。我个人还是倾向于把配置和命令都沉淀在项目里这样任何一个人拉下代码一条maven命令就能保证生成结果完全一致。第三种写一个独立Main方法动态生成适合需要把生成逻辑集成到自动化流程里的场景比如表结构发布之后自动触发代码生成。这种方式控制力最强可以通过Java代码动态构造数据库连接、读取表清单但配置成本也最高非必要不推荐。2.4 Java配置方式什么时候用从MBG 1.4.0开始官方支持纯Java代码的配置方式完全不用写XML直接通过Configuration对象和各个Generator类来编程式配置。这种方式的好处在于动态性比如你可以写一个通用的生成工具类从配置文件里读取数据库地址和表清单循环为多套环境生成代码。坏处是代码写起来比XML繁琐而且阅读性没有XML清晰。我目前的做法是常规项目坚持XML配置Maven插件因为配置直观、项目成员都熟悉只有那种需要频繁切换数据库环境、表数量非常多、且生成逻辑需要条件判断的项目才用Java配置方式。说到底MBG的配置本质是在描述“数据库连哪、代码生成到哪、表结构怎么映射”这三件事选哪种载体不重要把这三个问题回答清楚就行。3. 实操把一个订单表完整生成一遍说再多理论不如实际生成一次。接下来我就用一个比较常见的订单场景完整走一遍从准备到生成的流程。3.1 环境准备与依赖假设你这边的环境是JDK 8以上、Maven 3.6以上、MySQL 8.0数据库里已经建好了一张订单表。表结构大概长这样CREATE TABLE order_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 用户ID, total_amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 订单状态0待支付1已支付2已取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;项目本身是一个标准Spring Boot MyBatis的项目pom里已经引入了mybatis-spring-boot-starter。为了使用MBG我们再引入生成器相关的插件和依赖同时引入MySQL驱动。这里有个小诀窍生成器的驱动依赖尽量在plugin里单独声明不要和项目主依赖混在一起避免版本冲突。3.2 生成过程实录接下来在src/main/resources下新建generatorConfig.xml内容按照前面讲的模板改参数。几个关键点我再强调一下targetRuntime我一般用MyBatis3而不是MyBatis3Simple因为后者只生成最简单的CRUD没有ExampledefaultModelType用flat这样每个表只生成一个实体类不会拆出复杂的Key类代码更精简。然后执行mvn mybatis-generator:generate第一次跑你可能遇到一个经典报错Table xxx.order_info doesnt exist。如果你确定表确实存在大概率是连接的数据库不对或者tableName里带了库名前缀却没配置好转义。解决办法是把连接URL里的库名确认正确或者带上catalog配置。还有另一个常见报错是Unknown system variable tx_isolation这是MySQL 8.0下驱动版本过低导致的升级mysql-connector-j到8.0.x以上即可。生成结束后你会看到控制台输出一堆SUCCESS日志同时src/main/java/com/example/entity下多了OrderInfo.javasrc/main/java/com/example/dao下多了OrderInfoMapper.javasrc/main/resources/mapper下多了OrderInfoMapper.xml。三个文件一次成型整个过程不到十秒。3.3 生成后的产物长什么样打开OrderInfo.java你会发现字段类型都映射得挺准Long id、String orderNo、Long userId、BigDecimal totalAmount、Integer status、Date createTime、Date updateTime。而且带上了全参和无参构造方法也有对应的getter/setter。如果你在table里配置了comment注释它甚至会把数据库字段注释生成到Javadoc里这对团队协作帮助很大看代码不用再翻表结构文档。OrderInfoMapper.xml里默认生成的内容包括BaseResultMap、Base_Column_List、selectByExample、selectByPrimaryKey、deleteByExample、deleteByPrimaryKey、insert、insertSelective、countByExample、updateByExampleSelective、updateByPrimaryKeySelective这些基础方法。其中最常用的是带Selective后缀的方法它们会动态判断字段是否为null只为非null字段生成insert或update的列这在做部分更新时特别方便。OrderInfoMapper.java里的方法签名和XML里的id一一对应注意MBG生成的接口默认没有Mapper注解需要靠Spring Boot启动类上的MapperScan来统一扫描或者手动给接口加注解。这一步是接进Spring Boot最容易忘的地方。3.4 接进项目里的正确姿势把生成的代码接进Spring Boot项目正确姿势是在启动类上配置MapperScan(com.example.dao)让Spring能扫描到Mapper接口。然后在Service里注入OrderInfoMapper直接用。比如新增一个订单OrderInfo order new OrderInfo(); order.setOrderNo(NO202501010001); order.setUserId(1001L); order.setTotalAmount(new BigDecimal(299.00)); order.setStatus(0); orderMapper.insertSelective(order);获取主键回填这一点需要注意。MBG生成的insert方法默认是不回填自增主键的除非你在XML配置里加上useGeneratedKeys或者改用另一种方式利用MySQL的selectKey。其实MBG针对MySQL自增主键有一项内置支持在table节点配generatedKey即可table tableNameorder_info domainObjectNameOrderInfo generatedKey columnid sqlStatementMySql identitytrue/ /table配上之后insert执行完order.getId()就能拿到自增主键。这也是我在工程里要求每个生成表必须配置的一项否则后面做关联插入时还得再查一次主键很麻烦。4. 生成之后的进阶改造生的代码只是地基真正让项目好用要靠生成之后的改造和组合。这里我把几个在项目中验证过的常用套路分享出来。4.1 Example到底怎么用条件构造与复用MBG生成的OrderInfoExample是很多人用得不好又舍不得扔的东西。它本质上是一个强类型的条件容器内部有Criteria内部类createCriteria()方法会创建一条查询条件然后你可以在上面添加各种等于、不等于、大于、小于、like、in等条件。一个典型的按条件分页查询是这样OrderInfoExample example new OrderInfoExample(); OrderInfoExample.Criteria criteria example.createCriteria(); if (userId ! null) { criteria.andUserIdEqualTo(userId); } if (status ! null status 0) { criteria.andStatusEqualTo(status); } if (StringUtils.hasText(orderNo)) { criteria.andOrderNoLike(% orderNo %); } example.setOrderByClause(create_time desc); ListOrderInfo list orderInfoMapper.selectByExample(example);这里有个细节createCriteria()只能调用一次吗不是你可以调用多次例如example.or().andXxxEqualTo(...)来构造OR条件MBG会把这些criteria用OR连接起来这是它相对手写动态SQL的一个明显优势。另外example.setOrderByClause(create_time desc)可以传排序字符串不过要小心别拼用户输入否则会有SQL注入风险排序字段建议走白名单。如果你的项目里已经用MyBatis-Plus你会发现Example的写法和MP的LambdaQueryWrapper很像两者可以互相替代。不过Example的优势在于它和MBG生成的XML天生配套即使不引入MP也能获得类似的体验。我经常在新项目里先只用MBG等确实需要BaseMapper的便捷方法时再叠加MP这样依赖始终保持最小。4.2 分页、缓存、逻辑删除的接入首先生成的Mapper默认没有缓存标签。这意味着如果你直接调用selectByPrimaryKey每次都会打数据库。对于访问频繁且变更不频繁的数据可以在OrderInfoMapper.xml的命名空间里手动加上cache evictionLRU flushInterval60000 size512 readOnlytrue/加上之后MyBatis会为这个Mapper启用二级缓存查询结果会缓存在JVM堆里同一命名空间下的更新操作会自动清空缓存。要注意readOnlytrue意味着缓存中直接返回对象引用调用方修改对象会污染缓存所以读多写少且数据结构简单的场景才建议这样配如果对象会被修改得用readOnlyfalse开启序列化拷贝但那样会要求实体类实现Serializable。分页方面我强烈建议直接引入PageHelper而不是在Example里手动写limit。PageHelper用起来非常简单PageHelper.startPage(pageNum, pageSize); ListOrderInfo list orderInfoMapper.selectByExample(example); PageInfoOrderInfo pageInfo new PageInfo(list);注意一点PageHelper.startPage只对下一条查询生效所以中间绝对不能有其它SQL执行否则分页条件会被错误的SQL吃掉。拿这个分页和MBG结合是我在项目里最常用的列表查询组合稳定可靠。逻辑删除的接入推荐用columnOverride 拦截器配合或者干脆在业务层兜底。最简单的做法是给表加一个deleted字段然后所有查询都手动带上andDeletedEqualTo(0)但这样非常容易漏。更好的思路是写一个MyBatis拦截器拦截Executor的query方法自动往SQL里追加deleted0条件这样几乎不用改业务代码。关于拦截器我放到后面细说。4.3 与MyBatis-Plus衔接的注意事项我相信不少人和我一样既想享受MBG生成XML的便利又想要MP的BaseMapper。实际操作很简单把生成的OrderInfoMapper接口继承关系改一下让它扩展BaseMapperOrderInfopublic interface OrderInfoMapper extends BaseMapperOrderInfo { ListOrderInfo selectOrderWithDetails(Param(orderId) Long orderId); }这样OrderInfoMapper既有MP提供的selectById、selectList等通用方法又能保留XML里手写的复杂查询方法。需要注意几个坑第一MP的BaseMapper和MBG生成的insert、updateByPrimaryKey方法同名但行为可能不一致建议清理掉XML中与MP同名的方法避免逻辑混乱第二MP的实体主键默认识别TableId注解所以得在生成的实体类id字段上手动加上TableId(type IdType.AUTO)否则MP不知道主键生成策略第三逻辑删除字段需要配置MP的TableLogic注解。我实际的做法是MBG照常生成三件套然后对Mapper接口做一次小改造加上extends BaseMapperOrderInfoXML里只保留自定义的复杂SQL。这套组合已经在我好几个项目里稳定跑了两年多非常好用。4.4 拦截器把通用逻辑从业务代码里剥离出来说到拦截器这里展开讲讲。MyBatis的拦截器可以在SQL执行前后插入逻辑最常见的场景就是打印完整SQL、自动填充创建时间、实现逻辑删除。拦截器实现的原理基于MyBatis对四大核心对象Executor、StatementHandler、ParameterHandler、ResultSetHandler的动态代理你通过Intercepts注解指定拦截目标和方法然后在intercept方法里拿到Invocation通过Plugin.wrap包装成代理对象。一个打印SQL的简单拦截器长这样Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class SqlLogInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); String sql boundSql.getSql().replaceAll(\\s, ); System.out.println(SQL: sql); return invocation.proceed(); } }这里只打印了未带参数值的SQL如果想打印完整可执行SQL还得遍历BoundSql.getParameterMappings()取出参数替换占位符。市面上有mybatis-log-plugin这类工具干的就是这件事从输出日志里还原完整SQL。我建议团队里至少要有一个人把这块配置好否则排查问题时拿不到真实SQL效率太低了。5. 踩坑实录与排查手册最后这部分我整理一下这几年用MBG遇到的高频问题以及对应的排查思路。5.1 常见问题速查表问题现象根本原因解决方案生成的实体类没有注释未配置comment或数据库表/字段无注释table节点加commentGenerator配置或先在数据库补全注释找不到JDBC驱动Maven插件缺少驱动依赖在plugin的dependencies中加入mysql-connector-j生成的Mapper接口报红项目没有启用Mapper扫描启动类加MapperScan或在接口加Mapper表名带库名前缀生成失败MySQL 8下catalog/schema处理不一致在jdbcConnection配置property namenullCatalogMeansCurrent valuetrue/生成内容覆盖了手写代码未设置覆盖策略或生成物包含手写段手工代码放到生成文件之外如继承扩展或独立文件tinyint(1)生成成Byte默认类型映射规则如此使用columnOverride强制转为Boolean表名字段名是SQL关键字例如order、desc、ranktable节点配置delimitedIdentifiers或使用反引号生成速度慢、卡死数据库连接池或驱动问题检查连接URL和网络配置超时参数这些坑几乎每个用MBG的团队都会踩到提前知道能省不少调试时间。5.2 三个容易忽视的细节第一个细节是关于defaultModelType的。很多教程用的默认值是conditional这会导致简单的单主键表生成一个实体类但复合主键表会自动生成一个主键类文件数量突然变多。我统一用flat之后每个表固定一个实体类代码结构可预测性强也方便写自动化脚本处理。第二个细节是enableSubPackages。它在目标包名的基础上按照表所属的catalog/schema再拆一层子目录。如果你的项目没有多库需求这个属性建议保持默认值false以免生成的包路径层层嵌套看着心烦。但如果你确实管理着多个业务库开着它反而能让代码按库分隔泾渭分明。第三个细节是ResultMap的autoMapping。默认生成的ResultMap是显式列映射一旦实体类和表字段对不上容易漏值。用resultMap加autoMappingtrue可以开启自动映射剩余字段但前提是实体属性和列名能按驼峰转换规则匹配。建议在mybatis-config里打开mapUnderscoreToCamelCase让这两套命名体系统一减少配置量。5.3 我对MBG的使用建议如果你现在正准备在一套新项目里用MBG我的建议是做好三件事第一规范数据库设计字段注释写全、命名统一、主键策略明确这是生成高质量代码的基石第二在pom里固定好MBG插件和驱动版本把generatorConfig.xml纳入版本管理让所有人生成器版本一致第三生成代码时不要直接改生成的三件套遇到特殊需求要么通过columnOverride、table配置解决要么把自定义逻辑写在Service层或自定义XML方法里保证下次生成不被覆盖。把这套流程跑顺之后你会发现建一张新表变成了一件很惬意的事建表、改配置、跑一条命令、写业务逻辑把手动写基础代码的时间全部省下来专心处理真正有挑战的部分。顺带说一句MBG本身也是学习MyBatis底层原理的好范例你去看它生成的XML里的script动态SQL逻辑对比MyBatis源码里SqlSource的解析过程会对整个框架的理解深一个层次。后续我计划再写一篇关于MyBatis缓存机制和拦截器扩展的文章把今天提到的拦截器部分展开聊到时候欢迎继续来交流。