Mybatis源码阅读(二):动态节点解析2.1 —— SqlSource和SqlNode

*************************************优雅的分割线 **********************************

分享一波:程序员赚外快-必看的巅峰干货

如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程

请关注微信公众号:HB荷包
在这里插入图片描述
一个能让你学习技术和赚钱方法的公众号,持续更新

前言

前面的文章介绍了mybatis核心配置文件和mapper文件的解析,之后因为加班比较重,加上个人也比较懒,一拖就是将近半个月,今天抽空开始第二部分的阅读。

由前面的文章可知,mapper文件中定义的Sql节点会被解析成MappedStatement,其中的SQL语句会被解析成SqlSource。而Sql语句中定义的动态sql节点(如if节点、foreach节点)会被解析成SqlNode。SqlNode节点的解析中会使用到Ognl表达式(没错就是是struts2用的那玩意。本以为随着struts2和jsp淡出开发环境,这种动态标签也会随之过时,没想到mybatis里依然沿用了ognl),这个内容介绍起来有点麻烦,因此感兴趣的读者请自行了解一下。
SqlSource

Sql节点中的Sql语句会被解析成SqlSource,SqlSource接口中只定义了一个方法 getBoundSql 。该方法用于表示解析后的Sql语句(带问号)。

/**

  • 该接口用于标识映射文件或者注解中定义的sql语句

  • 这里的sql可能带有#{}等标志

  • @author Clinton Begin
    */
    public interface SqlSource {

    /**

    • 可执行的sql
    • @param parameterObject
    • @return
      */
      BoundSql getBoundSql(Object parameterObject);
      }

[点击并拖拽以移动]

SqlSource的继承关系如下图所示。每个实现类都比较简单,下面只做简单的说明。

DynamicSqlSource用于处理动态语句(带有动态sql标签),RawSqlSource用于处理静态语句(没有动态sql标签),二者最终会解析成StaticSqlSource。StaticSqlSource可能会带有问号。这里暂时只将代码简单的贴出来,部分内容需要结合后面才可以加注释(如SqlNode)

/**

  • 处理静态sql语句

  • @since 3.2.0

  • @author Eduardo Macarron
    */
    public class RawSqlSource implements SqlSource {

    private final SqlSource sqlSource;

    public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
    }

    public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
    }

    private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
    }
    }

/**

  • 负责解析动态sql语句

  • 包含#{}占位符

  • @author Clinton Begin
    */
    public class DynamicSqlSource implements SqlSource {

    private final Configuration configuration;
    private final SqlNode rootSqlNode;

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
    }

}

/**

  • 经过DynamicSqlSource和RawSqlSource处理后

  • 这里存放的sql可能含有?占位符

  • @author Clinton Begin
    */
    public class StaticSqlSource implements SqlSource {

    private final String sql;
    private final List parameterMappings;
    private final Configuration configuration;

    public StaticSqlSource(Configuration configuration, String sql) {
    this(configuration, sql, null);
    }

    public StaticSqlSource(Configuration configuration, String sql, List parameterMappings) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.configuration = configuration;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
    }

}

ProviderSqlSource暂时不贴出来(还没读到这里)
DynamicContext

DynamicContext用于记录解析动态Sql时产生的Sql片段。这里也先将主要代码放出来。

/**

  • 用于记录解析动态SQL语句之后产生的SQL语句片段

  • 可以认为它是一个用于记录动态SQL语句解析生产的容器

  • @author Clinton Begin
    */
    public class DynamicContext {

    public static final String PARAMETER_OBJECT_KEY = “_parameter”;
    public static final String DATABASE_ID_KEY = “_databaseId”;

    static {
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
    }

    /**

    • 参数上下文
      /
      private final ContextMap bindings;
      /
      *
    • 在SQL弄得解析动态SQL时,会将解析后的SQL语句片段添加到该属性总保存
    • 最终拼凑出一条完整的SQL
      */
      private final StringJoiner sqlBuilder = new StringJoiner(" ");
      private int uniqueNumber = 0;

    /**

    • 构造中初始化bindings集合
    • @param configuration
    • @param parameterObject 运行时用户传入的参数。
      */
      public DynamicContext(Configuration configuration, Object parameterObject) {
      if (parameterObject != null && !(parameterObject instanceof Map)) {
      // 非Map就去找对应的类型处理器
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
      bindings = new ContextMap(metaObject, existsTypeHandler);
      } else {
      bindings = new ContextMap(null, false);
      }
      bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
      bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
      }

    public Map<String, Object> getBindings() {
    return bindings;
    }

    public void bind(String name, Object value) {
    bindings.put(name, value);
    }

    /**

    • 追加SQL片段
    • @param sql
      */
      public void appendSql(String sql) {
      sqlBuilder.add(sql);
      }

    /**

    • 获取解析后的SQL语句
    • @return
      */
      public String getSql() {
      return sqlBuilder.toString().trim();
      }

    public int getUniqueNumber() {
    return uniqueNumber++;
    }

}

SqlNode

SqlNode表示Sql节点中的动态Sql。该类(接口)只有一个apply方法,用于解析动态Sql节点,并调用DynamicContext的appendSql方法去拼接sql语句。

/**

  • @author Clinton Begin
    */
    public interface SqlNode {

    /**

    • 根据用户传入的实参去解析动态SQL节点
    • 并调用DynamicContext.appendSql将解析后的SQL片段
    • 追加到DynamicContext.sqlBuilder保存
    • @param context
    • @return
      */
      boolean apply(DynamicContext context);
      }

SqlNode实现类很多,如图所示。光看实现类的名称,想必大家都可以猜出这些实现类的作用了。下面将对这些实现类一一解释

StaticTextSqlNode使用text字段记录非动态Sql节点,apply方法直接将text字段追加到DynamicContext.sqlBuilder;MixedSqlNode中使用contents字段存放子节点的动态sql,apply方法则是遍历contents去调用每个SqlNode的apply方法,代码都比较简单就不贴出来了。
TextSqlNode

TextSqlNode表示包含的sql节点,isDynamic方法用于检测sql中是否包含{}的sql节点,isDynamic方法用于检测sql中是否包含sqlisDynamicsql{}占位符。该类的apply方法会使用GenericTokenParser将占位符解析成实际意义的参数值,因此{}占位符解析成实际意义的参数值,因此{}在mybatis中会有注入风险,应当慎用,尽量用于非前端传递的参数。这里比较特殊的场景就是order by。order by后面只能使用${}占位符,因此前端操作排序列时,务必要做防注入处理。

/**

  • 包含${}的sql

  • @author Clinton Begin
    */
    public class TextSqlNode implements SqlNode {
    private final String text;
    private final Pattern injectionFilter;

    public TextSqlNode(String text) {
    this(text, null);
    }

    public TextSqlNode(String text, Pattern injectionFilter) {
    this.text = text;
    this.injectionFilter = injectionFilter;
    }

    public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
    }

    @Override
    public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
    }

    private GenericTokenParser createParser(TokenHandler handler) {
    // 这里标识解析的是占位符returnnewGenericTokenParser("{}占位符 return new GenericTokenParser("returnnewGenericTokenParser("{", “}”, handler);
    }

    private static class BindingTokenParser implements TokenHandler {

     private DynamicContext context;private Pattern injectionFilter;public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {this.context = context;this.injectionFilter = injectionFilter;}@Overridepublic String handleToken(String content) {// 获取用户提供的实参Object parameter = context.getBindings().get("_parameter");if (parameter == null) {context.getBindings().put("value", null);} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {context.getBindings().put("value", parameter);}// 通过ognl解析content的值Object value = OgnlCache.getValue(content, context.getBindings());String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"checkInjection(srtValue);return srtValue;}private void checkInjection(String value) {if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());}}
    

    }

    private static class DynamicCheckerTokenParser implements TokenHandler {

     private boolean isDynamic;public DynamicCheckerTokenParser() {// Prevent Synthetic Access}public boolean isDynamic() {return isDynamic;}@Overridepublic String handleToken(String content) {this.isDynamic = true;return null;}
    

    }

}

IfSqlNode

该类表示mybatis中的if标签。if标签中使用的其实就是Ognl语句,因此可以有一些很花哨的写法,如调用参数的equals方法等,这里不对Ognl表达式做过多的介绍。

/**

  • if节点

  • @author Clinton Begin
    /
    public class IfSqlNode implements SqlNode {
    /
    *

    • if节点的test表达式值
      /
      private final ExpressionEvaluator evaluator;
      /
      *
    • if节点的test表达式
      /
      private final String test;
      /
      *
    • if节点的子节点
      */
      private final SqlNode contents;

    public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
    }

    @Override
    public boolean apply(DynamicContext context) {
    // 检测表达式是否为true,来决定是否执行apply方法
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
    contents.apply(context);
    return true;
    }
    return false;
    }

}

TrimSqlNode

trimSqlNode用于根据解析结果添加或删除后缀活前缀。

/**

  • 根据解析结果添加或删除后缀或前缀

  • @author Clinton Begin
    */
    public class TrimSqlNode implements SqlNode {

    /**

    • trim节点的子节点
      /
      private final SqlNode contents;
      /
      *
    • 前缀
      /
      private final String prefix;
      /
      *
    • 后缀
      /
      private final String suffix;
      /
      *
    • 如果trim节点包裹的SQL是空语句,删除指定的前缀,如where
      /
      private final List prefixesToOverride;
      /
      *
    • 如果trim节点包裹的SQL是空语句,删除指定的后缀,如逗号
      */
      private final List suffixesToOverride;
      private final Configuration configuration;

    public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
    this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
    }

    protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List prefixesToOverride, String suffix, List suffixesToOverride) {
    this.contents = contents;
    this.prefix = prefix;
    this.prefixesToOverride = prefixesToOverride;
    this.suffix = suffix;
    this.suffixesToOverride = suffixesToOverride;
    this.configuration = configuration;
    }

    @Override
    public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    boolean result = contents.apply(filteredDynamicContext);
    // 处理前缀和后缀
    filteredDynamicContext.applyAll();
    return result;
    }

    /**

    • 对prefixOverrides和suffixOverride属性解析
    • 并初始化两个Override集合
    • @param overrides
    • @return
      */
      private static List parseOverrides(String overrides) {
      if (overrides != null) {
      // 使用|分隔
      final StringTokenizer parser = new StringTokenizer(overrides, “|”, false);
      final List list = new ArrayList<>(parser.countTokens());
      while (parser.hasMoreTokens()) {
      list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
      }
      return list;
      }
      return Collections.emptyList();
      }

    private class FilteredDynamicContext extends DynamicContext {
    /**
    * 上下文对象
    /
    private DynamicContext delegate;
    /
    *
    * 标识已经处理过的前缀和后缀
    /
    private boolean prefixApplied;
    private boolean suffixApplied;
    /
    *
    * 记录子节点解析后的结果
    */
    private StringBuilder sqlBuffer;

     public FilteredDynamicContext(DynamicContext delegate) {super(configuration, null);this.delegate = delegate;this.prefixApplied = false;this.suffixApplied = false;this.sqlBuffer = new StringBuilder();}public void applyAll() {sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);if (trimmedUppercaseSql.length() > 0) {applyPrefix(sqlBuffer, trimmedUppercaseSql);applySuffix(sqlBuffer, trimmedUppercaseSql);}delegate.appendSql(sqlBuffer.toString());}@Overridepublic Map<String, Object> getBindings() {return delegate.getBindings();}@Overridepublic void bind(String name, Object value) {delegate.bind(name, value);}@Overridepublic int getUniqueNumber() {return delegate.getUniqueNumber();}@Overridepublic void appendSql(String sql) {sqlBuffer.append(sql);}@Overridepublic String getSql() {return delegate.getSql();}/*** 处理前缀** @param sql sql* @param trimmedUppercaseSql 小写sql*/private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {if (!prefixApplied) {prefixApplied = true;if (prefixesToOverride != null) {for (String toRemove : prefixesToOverride) {// 遍历prefixesToOverride,如果以其中的某项开头就从SQL语句开头剔除if (trimmedUppercaseSql.startsWith(toRemove)) {sql.delete(0, toRemove.trim().length());break;}}}if (prefix != null) {sql.insert(0, " ");sql.insert(0, prefix);}}}/*** 处理后缀。* @param sql* @param trimmedUppercaseSql*/private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {if (!suffixApplied) {suffixApplied = true;if (suffixesToOverride != null) {for (String toRemove : suffixesToOverride) {if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {int start = sql.length() - toRemove.trim().length();int end = sql.length();sql.delete(start, end);break;}}}if (suffix != null) {sql.append(" ");sql.append(suffix);}}}
    

    }

}

WhereSqlNode&SetSqlNode

WhereSqlNode和SetSqlNode分别表示where节点和set节点。这两个类继承了TrimSqlNode,因此自带处理前后缀的功能。

WhereSqlNode将and、or两个关键字作为需要删除的前缀。当where的第一个条件以这两个开头时,会将and或者or删除。而SetSqlNode则会删除前缀或者后缀的嘤文逗号。这里只贴出WhereSqlNode代码。

/**

  • where节点。继承了TrimSqlNode

  • 因此where节点自带处理前缀后缀功能

  • @author Clinton Begin
    */
    public class WhereSqlNode extends TrimSqlNode {

    /**

    • 设置前缀是OR和AND,因此解析后的SQL如果以这俩开头就会删掉前缀
      */
      private static List prefixList = Arrays.asList("AND ", "OR ", “AND\n”, “OR\n”, “AND\r”, “OR\r”, “AND\t”, “OR\t”);

    public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, “WHERE”, prefixList, null, null);
    }

}

ForeachSqlNode

在动态Sql语句中构建in条件时,往往需要遍历一个集合,因此使用foreach标签。这里需要着重介绍一下FilteredDynamicContext这个内部类。该类继承了DynamicContext,用来处理foreach中的#{}占位符。这里是对其不完全的处理。如#{item}会被处理乘#{__frch_item_index值}这种格式,用来表示遍历中的每一项。

/**

  • forEach节点

  • @author Clinton Begin
    */
    public class ForEachSqlNode implements SqlNode {
    public static final String ITEM_PREFIX = “_frch”;

    /**

    • 判断循环终止的条件
      /
      private final ExpressionEvaluator evaluator;
      /
      *
    • 迭代的集合表达式
      /
      private final String collectionExpression;
      /
      *
    • 该节点下的节点
      /
      private final SqlNode contents;
      /
      *
    • 循环前以什么开头
      /
      private final String open;
      /
      *
    • 循环后以什么结束
      /
      private final String close;
      /
      *
    • 循环过程中的分隔符
      /
      private final String separator;
      /
      *
    • 每次循环的变量名
      /
      private final String item;
      /
      *
    • 当前迭代次数
      */
      private final String index;
      private final Configuration configuration;

    public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
    }

    @Override
    public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
    return true;
    }
    boolean first = true;
    // 循环之前添加open指定的字符串
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
    DynamicContext oldContext = context;
    if (first || separator == null) {
    // 是第一个循环,并且没有间隔符
    context = new PrefixedContext(context, “”);
    } else {
    context = new PrefixedContext(context, separator);
    }
    int uniqueNumber = context.getUniqueNumber();
    // 将index和item添加到DynamicContext.bindings集合
    if (o instanceof Map.Entry) {
    @SuppressWarnings(“unchecked”)
    Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
    applyIndex(context, mapEntry.getKey(), uniqueNumber);
    applyItem(context, mapEntry.getValue(), uniqueNumber);
    } else {
    applyIndex(context, i, uniqueNumber);
    applyItem(context, o, uniqueNumber);
    }
    // 调用子节点的apply急需处理
    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
    if (first) {
    first = !((PrefixedContext) context).isPrefixApplied();
    }
    context = oldContext;
    i++;
    }
    // 拼接close
    applyClose(context);
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
    }

    private void applyIndex(DynamicContext context, Object o, int i) {
    if (index != null) {
    context.bind(index, o);
    context.bind(itemizeItem(index, i), o);
    }
    }

    private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
    context.bind(item, o);
    context.bind(itemizeItem(item, i), o);
    }
    }

    private void applyOpen(DynamicContext context) {
    if (open != null) {
    context.appendSql(open);
    }
    }

    private void applyClose(DynamicContext context) {
    if (close != null) {
    context.appendSql(close);
    }
    }

    private static String itemizeItem(String item, int i) {
    return ITEM_PREFIX + item + “_” + i;
    }

    /**

    • 处理#{}(不完全处理)
      */
      private static class FilteredDynamicContext extends DynamicContext {
      private final DynamicContext delegate;
      private final int index;
      private final String itemIndex;
      private final String item;

      public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item, int i) {
      super(configuration, null);
      this.delegate = delegate;
      this.index = i;
      this.itemIndex = itemIndex;
      this.item = item;
      }

      @Override
      public Map<String, Object> getBindings() {
      return delegate.getBindings();
      }

      @Override
      public void bind(String name, Object value) {
      delegate.bind(name, value);
      }

      @Override
      public String getSql() {
      return delegate.getSql();
      }

      /**

      • 这里会将#{item}占位符解析成#{__frch_item_index值}

      • @param sql
        /
        @Override
        public void appendSql(String sql) {
        GenericTokenParser parser = new GenericTokenParser("#{", “}”, content -> {
        String newContent = content.replaceFirst("^\s
        " + item + “(?![^.,:\s])”, itemizeItem(item, index));
        if (itemIndex != null && newContent.equals(content)) {
        newContent = content.replaceFirst("^\s*" + itemIndex + “(?![^.,:\s])”, itemizeItem(itemIndex, index));
        }
        return “#{” + newContent + “}”;
        });

        delegate.appendSql(parser.parse(sql));
        }

      @Override
      public int getUniqueNumber() {
      return delegate.getUniqueNumber();
      }

    }

    private class PrefixedContext extends DynamicContext {
    private final DynamicContext delegate;
    private final String prefix;
    private boolean prefixApplied;

     public PrefixedContext(DynamicContext delegate, String prefix) {super(configuration, null);this.delegate = delegate;this.prefix = prefix;this.prefixApplied = false;}public boolean isPrefixApplied() {return prefixApplied;}@Overridepublic Map<String, Object> getBindings() {return delegate.getBindings();}@Overridepublic void bind(String name, Object value) {delegate.bind(name, value);}@Overridepublic void appendSql(String sql) {if (!prefixApplied && sql != null && sql.trim().length() > 0) {delegate.appendSql(prefix);prefixApplied = true;}delegate.appendSql(sql);}@Overridepublic String getSql() {return delegate.getSql();}@Overridepublic int getUniqueNumber() {return delegate.getUniqueNumber();}
    

    }

}

剩余的如ChooseSqlNode请读者自行阅读,代码也都比较容易理解。
结语

本次文章只是介绍一下动态sql解析时常用的类和接口,之后的文章对动态sql进行介绍时将不再对这些类进行赘述。

最后说一些闲话。

其实坚持写博客是一件很难的事情。七月份入职以来,便开始考虑写博客的事,起初不知道从哪写起,博客质量并不高。后来慢慢爱上了阅读源码这件事。其实mybatis源码我已经参照某本书读完了,但是阅读完之后我并没有觉得有何收获和见解,对源码的理解也比较浅显,因此便想着通过撰写博客的方式去加深对源码的认知。Mybatis插件机制是很重要的特性,而想编写一个好的插件就需要对源码有深刻的理解,因此源码不得不读,对于一个java程序员来说这也是必修课。在这几篇博客的撰写下,我慢慢养成了写博客的习惯,也知道什么该写,什么不该写。博客中大部分的内容其实都在代码注释上,因此显得博客内容不多,需要阅读者仔细阅读代码注释(但愿我的博客有人看吧。)。养成一个习惯不容易,这段时间划水的过程中对撰写博客这件事也有所懈怠(说实话差点都忘了我还开了这么大一个坑。)

*************************************优雅的分割线 **********************************

分享一波:程序员赚外快-必看的巅峰干货

如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程

请关注微信公众号:HB荷包
在这里插入图片描述
一个能让你学习技术和赚钱方法的公众号,持续更新

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/536980.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

k8s边缘节点_边缘计算,如何啃下集群管理这块硬骨头?

导读边缘计算平台&#xff0c;旨在将边缘端靠近数据源的计算单元纳入到中心云&#xff0c;实现集中管理&#xff0c;将云服务部署其上&#xff0c;及时响应终端请求。然而&#xff0c;成千上万的边缘节点散布于各地&#xff0c;例如银行网点、车载节点等&#xff0c;节点数量甚…

Mybatis源码阅读(二):动态节点解析2.2 —— SqlSourceBuilder与三种SqlSource

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

搞懂toString()与valueOf()的区别

一、toString&#xff08;&#xff09; 作用&#xff1a;toString&#xff08;&#xff09;方法返回一个表示改对象的字符串&#xff0c;如果是对象会返回&#xff0c;toString() 返回 “[object type]”,其中type是对象类型。 二、valueOf( ) 作用&#xff1a;valueOf房啊发返…

oracle入库的速度能到多少_倒车入库别练复杂了,其实就这两点

教练总会让学员反复练倒车入库&#xff0c;但不少学员都会有这样的疑惑&#xff1a;为什么每一次倒库结果都不一样&#xff0c;倒车入库的练习重点是什么&#xff1f;倒车入库是科二的重点及难点&#xff0c;但只要掌握以下两个关键&#xff0c;顺利通过真不难&#xff1a;01方…

Mybatis源码阅读(三):结果集映射3.1 —— ResultSetBuilder与简单映射

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

kdj买卖指标公式源码_通达信指标公式源码MACD背离KDJ背离指标

N1:5;N2:10;N3:21;N4:60;牛熊:EMA(CLOSE,N4),COLORGREEN,LINETHICK3;DIFF:EMA(CLOSE,12) - EMA(CLOSE,26);DEA:EMA(DIFF,8);A1:BARSLAST(REF(CROSS(DIFF,DEA),1)); B1:REF(C,A11)>C AND REF(DIFF,A11)DRAWTEXT(IF(B1>0,1,0),L-0.1,MACD底背),COLORGREEN;RSV:(CLOSE-LLV(L…

Mybatis源码阅读(三):结果集映射3.2 —— 嵌套映射

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

gridview获取选中行数据_Word转Excel,不想熬夜加班,那就掌握这个数据清洗方法...

私信回复关键词【福利】~获取丰富办公资源&#xff0c;助你高效办公早下班&#xff01;小伙伴们&#xff0c;大家好&#xff0c;我是专治各种疑难杂「数」的农夫~今天&#xff0c;我就为大家介绍一种高效的数据清洗方法&#xff0c;助你告别熬夜加班&#xff0c;拥抱美好的夜晚…

Mybatis源码阅读(三):结果集映射3.3 —— 主键生成策略

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

list最大容量_Java 基础(四)集合源码解析 List

List 接口前面我们学习了Iterator、Collection&#xff0c;为集合的学习打下了基础&#xff0c;现在我们来学习集合的第一大体系 List。List 是一个接口&#xff0c;定义了一组元素是有序的、可重复的集合。List 继承自 Collection&#xff0c;较之 Collection&#xff0c;List…

Mybatis源码阅读(四):核心接口4.1——StatementHandler

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

Shell学习之结合正则表达式与通配符的使用(五)

Shell学习之结合正则表达式与通配符的使用 目录 通配符 正则表达式与通配符通配符通配符的使用正则表达式 正则表达式正则表达式的使用通配符 正则表达式与通配符 正则表达式用来在文件中匹配符合条件的字符串&#xff0c;正则是包含匹配。grep、awk、sed等命令可以支持正则表达…

Mybatis源码阅读(四):核心接口4.2——Executor(上)

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

接收xml参数_SpringBoot实战(二):接收xml请求

强烈推荐一个大神的人工智能的教程&#xff1a;http://www.captainbed.net/zhanghan【前言】最近在对接一个第三方系统&#xff0c;需要接收第三方系统的回调&#xff0c;而且格式为XML形式&#xff0c;之前自己一般接收的参数是Json形式&#xff0c;于是乎做个实验验证一下使用…

报错 插入更新_window如何解决mysql数据量过大导致的报错

window如何解决报错“The total number of locks exceeds the lock table size”第一大步&#xff0c;查看mysql配置信息在CMD中输入mysql -hlocalhost -uroot -p #如果设置了密码直接接在p 后面 show variables like %storage_engine%以下为结果可以看到InnoDB是MySQL的默认引…

Mybatis源码阅读(四):核心接口4.2——Executor(下)

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

Mybatis源码阅读(五 ):接口层——SqlSession

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

插入公式_一个小工具,彻底帮你搞定在Markdown中插入公式的问题

在编辑Markdown文档时&#xff0c;插入公式是一个挺麻烦的活儿。需要掌握LaTex语法。我自己看完语法后&#xff0c;直接放弃&#xff0c;这绝对是反人类的语法。&#xff08;好吧&#xff0c;是我不会用...&#xff09;但是&#xff0c;我相信你看了这篇文章后&#xff0c;绝对…

Mybatis源码阅读(一):Mybatis初始化1.2 —— 解析别名、插件、对象工厂、反射工具箱、环境

*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程 请关注微信公众号:HB荷包 一个能让你学习技术和赚钱方法的公众号,持续更…

Google 修改 Chrome API,防止隐身模式检测

开发四年只会写业务代码&#xff0c;分布式高并发都不会还做程序员&#xff1f; 在使用 Chrome 浏览网页时&#xff0c;某些网站会使用某种方法来确定访问者是否处于隐身模式&#xff0c;这是一种隐私泄漏行为。Google 目前正在考虑修改 Chrome 的相关 API&#xff0c;来杜绝…