一.前言

之前有个需求,是使ElasticSearch支持使用SQL进行简单查询,较新版本的ES已经支持该特性(不过貌似还是实验性质的?) ,而且git上也有elasticsearch-sql

插件,之所以决定手写一个,主要有两点原因:

1. 目前用的ES版本较老

2. elasticsearch-sql虽好,但比较复杂,代码也不易维护

3. 练练手

 二.技术选型

目前主流软件中通常使用ANTLR做词法语法分析,诸如著名的Hibernate,Spark,Hive等项目,之前因为工作原因也有所接触,不过如果只是解析标准SQL的话,

其实还有更好的选择,如使用Hibernate或阿里巴巴的数据库Druid(Druid采用了手写词法语法分析器的方案,这种方式当然比自动ANTLR生成的解析器性能高得多), 这里

我选择了第二种方案。

开始之前先看下我们可以通过Druid拿到的SQL语言的抽象语法树:

图片:https://www.jianshu.com/p/437aa22ea3ca

三.技术实现

首先我们创建一个SqlParser类,主流程都在parse方法中,该方法负责将一个SQL字符串解析(顺便说一句,Druid支持多种SQL方言,这里我选择了MySQL),

并返回SearchSourceBuilder对象,这是一个ElasticSearch提供的DSL构建器,以该对象作为参数,ES client端即可发起对ES 服务端搜索请求。

  1. /**
  2. *
  3. * @author fred
  4. *
  5. */
  6. public class SqlParser {
  7. private final static String dbType = JdbcConstants.MYSQL;
  8. private final static Logger logger = LoggerFactory.getLogger(SqlParser.class);
  9. private SearchSourceBuilder builder;
  10.  
  11. public SqlParser(SearchSourceBuilder builder) {
  12. this.builder = builder;
  13. }
  14. /**
  15. * 将SQL解析为ES查询
  16. */
  17. public SearchSourceBuilder parse(String sql) throws Exception {
  18. if (Objects.isNull(sql)) {
  19. throw new IllegalArgumentException("输入语句不得为空");
  20. }
  21. sql = sql.trim().toLowerCase();
  22. List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType);
  23. if (Objects.isNull(stmtList) || stmtList.size() != 1) {
  24. throw new IllegalArgumentException("必须输入一句查询语句");
  25. }
  26. // 使用Parser解析生成AST
  27. SQLStatement stmt = stmtList.get(0);
  28. if (!(stmt instanceof SQLSelectStatement)) {
  29. throw new IllegalArgumentException("输入语句须为Select语句");
  30. }
  31. SQLSelectStatement sqlSelectStatement = (SQLSelectStatement) stmt;
  32. SQLSelectQuery sqlSelectQuery = sqlSelectStatement.getSelect().getQuery();
  33. SQLSelectQueryBlock sqlSelectQueryBlock = (SQLSelectQueryBlock) sqlSelectQuery;
  34.  
  35. SQLExpr whereExpr = sqlSelectQueryBlock.getWhere();
  36.  
  37. // 生成ES查询条件
  38. BoolQueryBuilder bridge = QueryBuilders.boolQuery();
  39. bridge.must();
  40.  
  41. QueryBuilder whereBuilder = whereHelper(whereExpr); // 处理where
  42. bridge.must(whereBuilder);
  43. SQLOrderBy orderByExpr = sqlSelectQueryBlock.getOrderBy(); // 处理order by
  44. if (Objects.nonNull(orderByExpr)) {
  45. orderByHelper(orderByExpr, bridge);
  46. }
  47. builder.query(bridge);
  48. return builder;
  49. }

主流程很简单,拿到SQL字符串后,直接通过Druid API将其转换为抽象语法树,我们要求输入语句必须为Select语句。接下来是对where语句和order by语句的处理,

目前的难点其实主要在于如何将where语句映射到ES查询中。

先从简单的看起,如何处理order by呢?SQL语句中 order by显然可以允许用户根据多字段排序,所以排序字段肯定是一个List<排序字段>,我们要做的就是将这个List映射到

SearchSourceBuilder对象中。见下面代码:

  1. /**
  2. * 处理所有order by字段
  3. *
  4. * @param orderByExpr
  5. */
  6. private void orderByHelper(SQLOrderBy orderByExpr, BoolQueryBuilder bridge) {
  7. List<SQLSelectOrderByItem> orderByList = orderByExpr.getItems(); // 待排序的列
  8. for (SQLSelectOrderByItem sqlSelectOrderByItem : orderByList) {
  9. if (sqlSelectOrderByItem.getType() == null) {
  10. sqlSelectOrderByItem.setType(SQLOrderingSpecification.ASC); // 默认升序
  11. }
  12. String orderByColumn = sqlSelectOrderByItem.getExpr().toString();
  13. builder.sort(orderByColumn,
  14. sqlSelectOrderByItem.getType().equals(SQLOrderingSpecification.ASC) ? SortOrder.ASC
  15. : SortOrder.DESC);
  16. }
  17. }

通过Druid的API,我们很容易拿到了SQL语句中所有的排序字段,我们逐个遍历这些字段,拿到排序的列名字面量和顺序,传递给SearchSourceBuilder的sort方法,需注意的

是, 如果原始SQL中没有指定字段是顺序,我们默认升序。

接下来我们处理稍微有点麻烦的where语句,因为SQL语句被解析成了语法树,很自然的我们想到使用递归方式进行处理。 而通常在处理递归问题的时候,

我习惯于从递归的base case开始考虑,where语句中的运算符根据Druid API中的定义主要分为以下三种:

1. 简单二元运算符:包括逻辑处理,如and, or 和大部分关系运算(后续会详细讲)

2. between或not between运算符:我们可以简单的将其映射成ES中的Range Query

3. in, not in 运算符: 可以简单的映射成ES中的Term Query

通过Druid,我们可以很方便的获取每种运算中的运算符与操作数

  1. /**
  2. * 递归遍历“where”子树
  3. *
  4. * @return
  5. */
  6. private QueryBuilder whereHelper(SQLExpr expr) throws Exception {
  7. if (Objects.isNull(expr)) {
  8. throw new NullPointerException("节点不能为空!");
  9. }
  10. BoolQueryBuilder bridge = QueryBuilders.boolQuery();
  11. if (expr instanceof SQLBinaryOpExpr) { // 二元运算
  12. SQLBinaryOperator operator = ((SQLBinaryOpExpr) expr).getOperator(); // 获取运算符
  13. if (operator.isLogical()) { // and,or,xor
  14. return handleLogicalExpr(expr);
  15. } else if (operator.isRelational()) { // 具体的运算,位于叶子节点
  16. return handleRelationalExpr(expr);
  17. }
  18. } else if (expr instanceof SQLBetweenExpr) { // between运算
  19. SQLBetweenExpr between = ((SQLBetweenExpr) expr);
  20. boolean isNotBetween = between.isNot(); // between or not between ?
  21. String testExpr = between.testExpr.toString();
  22. String fromStr = formatSQLValue(between.beginExpr.toString());
  23. String toStr = formatSQLValue(between.endExpr.toString());
  24. if (isNotBetween) {
  25. bridge.must(QueryBuilders.rangeQuery(testExpr).lt(fromStr).gt(toStr));
  26. } else {
  27. bridge.must(QueryBuilders.rangeQuery(testExpr).gte(fromStr).lte(toStr));
  28. }
  29. return bridge;
  30. } else if (expr instanceof SQLInListExpr) { // SQL的 in语句,ES中对应的是terms
  31. SQLInListExpr siExpr = (SQLInListExpr) expr;
  32. boolean isNotIn = siExpr.isNot(); // in or not in?
  33. String leftSide = siExpr.getExpr().toString();
  34. List<SQLExpr> inSQLList = siExpr.getTargetList();
  35. List<String> inList = new ArrayList<>();
  36. for (SQLExpr in : inSQLList) {
  37. String str = formatSQLValue(in.toString());
  38. inList.add(str);
  39. }
  40. if (isNotIn) {
  41. bridge.mustNot(QueryBuilders.termsQuery(leftSide, inList));
  42. } else {
  43. bridge.must(QueryBuilders.termsQuery(leftSide, inList));
  44. }
  45. return bridge;
  46. }
  47. return bridge;
  48. }

上述第一种情况比较复杂,首先我们先看看运算符是逻辑运算的情况:

如下面的代码所示,如果运算符是逻辑运算符,我们需要对左右操作数分别递归,然后根据运算符类型归并结果:or可以映射成ES 中的Should,而and则映射成Must.

  1. /**
  2. * 逻辑运算符,目前支持and,or
  3. *
  4. * @return
  5. * @throws Exception
  6. */
  7. private QueryBuilder handleLogicalExpr(SQLExpr expr) throws Exception {
  8. BoolQueryBuilder bridge = QueryBuilders.boolQuery();
  9. SQLBinaryOperator operator = ((SQLBinaryOpExpr) expr).getOperator(); // 获取运算符
  10. SQLExpr leftExpr = ((SQLBinaryOpExpr) expr).getLeft();
  11. SQLExpr rightExpr = ((SQLBinaryOpExpr) expr).getRight();
  12.  
  13. // 分别递归左右子树,再根据逻辑运算符将结果归并
  14. QueryBuilder leftBridge = whereHelper(leftExpr);
  15. QueryBuilder rightBridge = whereHelper(rightExpr);
  16. if (operator.equals(SQLBinaryOperator.BooleanAnd)) {
  17. bridge.must(leftBridge).must(rightBridge);
  18. } else if (operator.equals(SQLBinaryOperator.BooleanOr)) {
  19. bridge.should(leftBridge).should(rightBridge);
  20. }
  21. return bridge;
  22. }

下面来讨论下第一种情况中,如果运算符是关系运算符的情况,我们知道,SQL中的关系运算主要就是一些比较运算符,诸如大于,小于,等于,Like等,这里我还加上了

正则搜索(不过貌似性能比较差,ES对正则搜索的限制颇多,不太建议使用)。

  1. /**
  2. * 大于小于等于正则
  3. *
  4. * @param expr
  5. * @return
  6. */
  7. private QueryBuilder handleRelationalExpr(SQLExpr expr) {
  8. SQLExpr leftExpr = ((SQLBinaryOpExpr) expr).getLeft();
  9. if (Objects.isNull(leftExpr)) {
  10. throw new NullPointerException("表达式左侧不得为空");
  11. }
  12. String leftExprStr = leftExpr.toString();
  13. String rightExprStr = formatSQLValue(((SQLBinaryOpExpr) expr).getRight().toString()); // TODO:表达式右侧可以后续支持方法调用
  14. SQLBinaryOperator operator = ((SQLBinaryOpExpr) expr).getOperator(); // 获取运算符
  15. QueryBuilder queryBuilder;
  16. switch (operator) {
  17. case GreaterThanOrEqual:
  18. queryBuilder = QueryBuilders.rangeQuery(leftExprStr).gte(rightExprStr);
  19. break;
  20. case LessThanOrEqual:
  21. queryBuilder = QueryBuilders.rangeQuery(leftExprStr).lte(rightExprStr);
  22. break;
  23. case Equality:
  24. queryBuilder = QueryBuilders.boolQuery();
  25. TermQueryBuilder eqCond = QueryBuilders.termQuery(leftExprStr, rightExprStr);
  26. ((BoolQueryBuilder) queryBuilder).must(eqCond);
  27. break;
  28. case GreaterThan:
  29. queryBuilder = QueryBuilders.rangeQuery(leftExprStr).gt(rightExprStr);
  30. break;
  31. case LessThan:
  32. queryBuilder = QueryBuilders.rangeQuery(leftExprStr).lt(rightExprStr);
  33. break;
  34. case NotEqual:
  35. queryBuilder = QueryBuilders.boolQuery();
  36. TermQueryBuilder notEqCond = QueryBuilders.termQuery(leftExprStr, rightExprStr);
  37. ((BoolQueryBuilder) queryBuilder).mustNot(notEqCond);
  38. break;
  39. case RegExp: // 对应到ES中的正则查询
  40. queryBuilder = QueryBuilders.boolQuery();
  41. RegexpQueryBuilder regCond = QueryBuilders.regexpQuery(leftExprStr, rightExprStr);
  42. ((BoolQueryBuilder) queryBuilder).mustNot(regCond);
  43. break;
  44. case NotRegExp:
  45. queryBuilder = QueryBuilders.boolQuery();
  46. RegexpQueryBuilder notRegCond = QueryBuilders.regexpQuery(leftExprStr, rightExprStr);
  47. ((BoolQueryBuilder) queryBuilder).mustNot(notRegCond);
  48. break;
  49. case Like:
  50. queryBuilder = QueryBuilders.boolQuery();
  51. MatchPhraseQueryBuilder likeCond = QueryBuilders.matchPhraseQuery(leftExprStr,
  52. rightExprStr.replace("%", ""));
  53. ((BoolQueryBuilder) queryBuilder).must(likeCond);
  54. break;
  55. case NotLike:
  56. queryBuilder = QueryBuilders.boolQuery();
  57. MatchPhraseQueryBuilder notLikeCond = QueryBuilders.matchPhraseQuery(leftExprStr,
  58. rightExprStr.replace("%", ""));
  59. ((BoolQueryBuilder) queryBuilder).mustNot(notLikeCond);
  60. break;
  61. default:
  62. throw new IllegalArgumentException("暂不支持该运算符!" + operator.toString());
  63. }
  64. return queryBuilder;
  65. }

到这里我们就完成了SQL转ES DSL的功能了(其实只是简单查询的转换),下面我们写几个Junit测试一下吧:

首先是简单的比较运算:

  1. public void normalSQLTest() {
  2. String sql = "select * from test where time>= 1";
  3. SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
  4. try {
  5. searchSourceBuilder = new SqlParser(searchSourceBuilder).parse(sql);
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. System.out.println(searchSourceBuilder);
  10. SearchSourceBuilder builderToCompare = new SearchSourceBuilder();
  11. QueryBuilder whereBuilder = QueryBuilders.rangeQuery("time").gte("");
  12. BoolQueryBuilder briage = QueryBuilders.boolQuery();
  13. briage.must();
  14. briage.must(whereBuilder);
  15. builderToCompare.query(briage);
  16. assertEquals(searchSourceBuilder,builderToCompare);
  17. }

下面是输出的ES 查询语句:

  1. {
  2. "query" : {
  3. "bool" : {
  4. "must" : [
  5. {
  6. "range" : {
  7. "time" : {
  8. "from" : "",
  9. "to" : null,
  10. "include_lower" : true,
  11. "include_upper" : true,
  12. "boost" : 1.0
  13. }
  14. }
  15. }
  16. ],
  17. "disable_coord" : false,
  18. "adjust_pure_negative" : true,
  19. "boost" : 1.0
  20. }
  21. }
  22. }

再来个带排序的:

  1. @Test
  2. public void normalSQLWithOrderByTest() {
  3. String sql = "select * from test where time>= 1 order by time desc";
  4. SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
  5. try {
  6. searchSourceBuilder = new SqlParser(searchSourceBuilder).parse(sql);
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. }
  10. System.out.println(searchSourceBuilder);
  11. SearchSourceBuilder builderToCompare = new SearchSourceBuilder();
  12. QueryBuilder whereBuilder = QueryBuilders.rangeQuery("time").gte("1");
  13. BoolQueryBuilder briage = QueryBuilders.boolQuery();
  14. briage.must();
  15. briage.must(whereBuilder);
  16. builderToCompare.sort("time",SortOrder.DESC);
  17. builderToCompare.query(briage);
  18. assertEquals(searchSourceBuilder,builderToCompare);
  19. }

between, in这些没什么区别,就不贴代码了,最后看看稍微复杂点儿,带逻辑运算的查询:

  1. @Test
  2. public void sqlLogicTest() {
  3. String sql = "select * from test where raw_log not like"+"'%aaa' && b=1 or c=0";
  4. SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
  5. try {
  6. searchSourceBuilder = new SqlParser(searchSourceBuilder).parse(sql);
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. }
  10. System.out.println(searchSourceBuilder);
  11. SearchSourceBuilder builderToCompare = new SearchSourceBuilder();
  12. QueryBuilder builder =QueryBuilders.matchPhraseQuery("raw_log","aaa");
  13.  
  14. BoolQueryBuilder briage1 = QueryBuilders.boolQuery();//raw log not like
  15. briage1.mustNot(builder);
  16.  
  17. BoolQueryBuilder briage2 = QueryBuilders.boolQuery(); //b=1
  18. briage2.must(QueryBuilders.termQuery("b","1"));
  19.  
  20. BoolQueryBuilder briage3 = QueryBuilders.boolQuery(); // not like and b=1
  21. briage3.must(briage1).must(briage2);
  22.  
  23. BoolQueryBuilder briage4 = QueryBuilders.boolQuery(); //c =0
  24. briage4.must(QueryBuilders.termQuery("c","0"));
  25.  
  26. BoolQueryBuilder briage5 = QueryBuilders.boolQuery(); // not like and b =1 or c =0
  27. briage5.should(briage3).should(briage4);
  28.  
  29. BoolQueryBuilder briage6 = QueryBuilders.boolQuery();
  30. briage6.must();
  31. briage6.must(briage5);
  32. builderToCompare.query(briage6);
  33. assertEquals(searchSourceBuilder,builderToCompare);
  34. }

下面是生成的查询语句:

  1. {
  2. "query" : {
  3. "bool" : {
  4. "must" : [
  5. {
  6. "bool" : {
  7. "should" : [
  8. {
  9. "bool" : {
  10. "must" : [
  11. {
  12. "bool" : {
  13. "must_not" : [
  14. {
  15. "match_phrase" : {
  16. "raw_log" : {
  17. "query" : "aaa",
  18. "slop" : 0,
  19. "boost" : 1.0
  20. }
  21. }
  22. }
  23. ],
  24. "disable_coord" : false,
  25. "adjust_pure_negative" : true,
  26. "boost" : 1.0
  27. }
  28. },
  29. {
  30. "bool" : {
  31. "must" : [
  32. {
  33. "term" : {
  34. "b" : {
  35. "value" : "1",
  36. "boost" : 1.0
  37. }
  38. }
  39. }
  40. ],
  41. "disable_coord" : false,
  42. "adjust_pure_negative" : true,
  43. "boost" : 1.0
  44. }
  45. }
  46. ],
  47. "disable_coord" : false,
  48. "adjust_pure_negative" : true,
  49. "boost" : 1.0
  50. }
  51. },
  52. {
  53. "bool" : {
  54. "must" : [
  55. {
  56. "term" : {
  57. "c" : {
  58. "value" : "0",
  59. "boost" : 1.0
  60. }
  61. }
  62. }
  63. ],
  64. "disable_coord" : false,
  65. "adjust_pure_negative" : true,
  66. "boost" : 1.0
  67. }
  68. }
  69. ],
  70. "disable_coord" : false,
  71. "adjust_pure_negative" : true,
  72. "boost" : 1.0
  73. }
  74. }
  75. ],
  76. "disable_coord" : false,
  77. "adjust_pure_negative" : true,
  78. "boost" : 1.0
  79. }
  80. }
  81. }

四.总结

本篇文章主要讲述了如何使用Druid实现SQL语句转换ES DSL进行搜索的功能,后续文章中会陆续完善这个功能,实现诸如聚合查询,分页查询等功能。

手写一个简单的ElasticSearch SQL转换器(一)的更多相关文章

  1. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

  2. 手写一个简单的starter组件

    spring-boot中有很多第三方包,都封装成starter组件,在maven中引用后,启动springBoot项目时会自动装配到spring ioc容器中. 思考: 为什么我们springBoot ...

  3. 手写一个简单的HashMap

    HashMap简介 HashMap是Java中一中非常常用的数据结构,也基本是面试中的"必考题".它实现了基于"K-V"形式的键值对的高效存取.JDK1.7之前 ...

  4. 手写一个简单到SpirngMVC框架

    spring对于java程序员来说,无疑就是吃饭到筷子.在每次编程工作到时候,我们几乎都离不开它,相信无论过去,还是现在或是未来到一段时间,它仍会扮演着重要到角色.自己对spring有一定的自我见解, ...

  5. jquery 手写一个简单浮窗的反面教材

    前言 初学jquery写的代码,陈年往事回忆一下. 正文 介绍一下大体思路 思路: 1.需要控制一块区域,这块区域一开始是隐藏的. 2.这个区域需要关闭按钮,同时我需要写绑定事件,关闭的时候让这块区域 ...

  6. 手写一个简单版的SpringMVC

    一 写在前面 这是自己实现一个简单的具有SpringMVC功能的小Demo,主要实现效果是; 自己定义的实现效果是通过浏览器地址传一个name参数,打印“my name is”+name参数.不使用S ...

  7. socket手写一个简单的web服务端

    直接进入正题吧,下面的代码都是我在pycharm中写好,再粘贴上来的 import socket server = socket.socket() server.bind(('127.0.0.1', ...

  8. 如何手写一个简单的LinkedList

    这是我写的第三个集合类了,也是简单的实现了一下基本功能,这次带来的是LinkedList的写法,需要注意的内容有以下几点: 1.LinkedList是由链表构成的,链表的核心即使data,前驱,后继 ...

  9. JQuery手写一个简单的轮播图

    做出来的样式: 没有切图,就随便找了一些图片来实现效果,那几个小星星萌不萌. 这个轮播图最主要的部分是animate(),可以先熟悉下这个方法. 代码我放到了github上,链接:https://gi ...

随机推荐

  1. linux环境下Nginx的配置及使用

    切换到目录/usr/local/nginx/sbin,/usr/local为nginx的默认安装目录 #启动 ./nginx #查看命令帮助 ./nginx -h 验证配置文件状态 ./nginx - ...

  2. 使用servlet+jdbc+MD5实现用户加密登录

    /** * 分析流程: * 1.前端页面提交登录请求 * 2.被web.xml拦截,进入到LoginServlet(有两种方式:方式一,在web.xml文件中配置servlet拦截器;方式二,不用在w ...

  3. 十分钟快速学会Matplotlib基本图形操作

    在学习Python的各种工具包的时候,看网上的各种教程总是感觉各种方法很多很杂,参数的种类和个数也十分的多,理解起来需要花费不少的时间. 所以我在这里通过几个例子,对方法和每个参数都进行详细的解释,这 ...

  4. vue 条件渲染方式

    1.通过class绑定 <div :class="{'div-class': this.align == 'center'}"></div> 对应的css ...

  5. MongoDB 基础教程CURD帮助类

    最近两天在学习MongoDB,强大的文档数据库.给我最大的感觉就是相比于SQL或者MSQ等传统的关系型数据库,在使用和配置上真的是简化了很多.无论是在集群的配置还是故障转移方面,都省去了许多繁琐的步骤 ...

  6. python正则表达式贪婪算法与非贪婪算法与正则表达式子模式的简单应用

    先引入一下百度百科对于正则表达式的概念: 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符.及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种 ...

  7. B/S 端 WebGL 3D 游戏机教程

    前言 摘要:2D 的俄罗斯方块已经被人玩烂了,突发奇想就做了个 3D 的游戏机,用来玩俄罗斯方块...实现的基本想法是先在 2D 上实现俄罗斯方块小游戏,然后使用 3D 建模功能创建一个 3D 街机模 ...

  8. js 验证数据类型的4中方法

    1.typeof  可以检验基本数据类型 但是引用数据类型(复杂数据类型)无用: 总结 : typeof  无法识别引用数据类型  包括 bull; 2.instanceof是一个二元运算符,左操作数 ...

  9. LeetCode正则表达式匹配

    题目描述 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 ...

  10. 快学Scala 第十七课 (trait 入门)

    trait 入门: trait类似于java的接口,不过比java接口功能更强大,可以有实体成员,抽象成员,实体方法,抽象方法. 如果需要混入的特质不止一个用with关键字. 带有特质的对象:(特质可 ...