关于mybatis:分享从Mybatis源码中学习到的10种设计模式

58次阅读

共计 12894 个字符,预计需要花费 33 分钟才能阅读完成。

作者:小傅哥
<br/> 博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!😄

一、前言:小镇卷码家

总有不少研发搭档问小傅哥:“为什么学设计模式、看框架源码、补技术常识,就一个一般的业务我的项目,会造飞机不也是天天写 CRUD 吗?”

你说的没错,但你天天写 CRUD,你感觉 烦不? 慌不? 是不是既放心本人没有失去技术成长,也胆怯未来没法用这些都是 CRUD 的我的项目去加入;述职、降职、问难,甚至可能要被迫面试时,本人手里一点干货也没有的状况。

所以你 / 我作为一个 小镇卷码家 ,当然要裁减本人的常识储备,否则 架构,架构思维不懂 设计,设计模式不会 源码、源码学习不深,最初就用一堆 CRUD 写简历吗?

二、源码:学设计模式

在 Mybatis 两万多行的框架源码实现中,应用了大量的设计模式来解耦工程架构中面对简单场景的设计,这些是设计模式的奇妙应用才是整个框架的精髓,这也是小傅哥喜爱卷源码的重要起因。通过小傅哥的整顿有如下 10 种设计模式的应用,如图所示

讲道理,如果只是把这 10 种设计模式背下来,等着下次面试的时候拿出来说一说,尽管能有点帮忙,不过这种学习形式就真的算是把路走窄了。就像你每说一个设计模式,能联想到这个设计模式在 Mybatis 的框架中,体现到哪个流程中的源码实现上了吗?这个源码实现的思路能不能用到你的业务流程开发里?别总说你的流程简略,用不上设计模式!难到因为有钱、富二代,就不考试吗?🤔

好啦,不扯淡了,接下来小傅哥就以《手写 Mybatis:渐进式源码实际》的学习,给大家列举出这 10 种设计模式,在 Mybatis 框架中都体现在哪里了!

  • 本文对应源码仓库:https://gitcode.net/KnowledgePlanet/doc/-/wikis/home

三、类型:创立型模式

1. 工厂模式

源码详见cn.bugstack.mybatis.session.SqlSessionFactory

public interface SqlSessionFactory {SqlSession openSession();

}

源码详见cn.bugstack.mybatis.session.defaults.DefaultSqlSessionFactory

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}

    @Override
    public SqlSession openSession() {
        Transaction tx = null;
        try {final Environment environment = configuration.getEnvironment();
            TransactionFactory transactionFactory = environment.getTransactionFactory();
            tx = transactionFactory.newTransaction(configuration.getEnvironment().getDataSource(), TransactionIsolationLevel.READ_COMMITTED, false);
            // 创立执行器
            final Executor executor = configuration.newExecutor(tx);
            // 创立 DefaultSqlSession
            return new DefaultSqlSession(configuration, executor);
        } catch (Exception e) {
            try {
                assert tx != null;
                tx.close();} catch (SQLException ignore) { }
            throw new RuntimeException("Error opening session.  Cause:" + e);
        }
    }

}

  • 工厂模式:简略工厂,是一种创立型设计模式,其在父类中提供一个创建对象的办法,容许子类决定实例对象的类型。
  • 场景介绍SqlSessionFactory 是获取会话的工厂,每次咱们应用 Mybatis 操作数据库的时候,都会开启一个新的会话。在会话工厂的实现中负责获取数据源环境配置信息、构建事务工厂、创立操作 SQL 的执行器,并最终返回会话实现类。
  • 同类设计SqlSessionFactoryObjectFactoryMapperProxyFactoryDataSourceFactory

2. 单例模式

源码详见cn.bugstack.mybatis.session.Configuration

public class Configuration {

    // 缓存机制,默认不配置的状况是 SESSION
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

    // 映射注册机
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);

    // 映射的语句,存在 Map 里
    protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();
    // 缓存, 存在 Map 里
    protected final Map<String, Cache> caches = new HashMap<>();
    // 后果映射,存在 Map 里
    protected final Map<String, ResultMap> resultMaps = new HashMap<>();
    protected final Map<String, KeyGenerator> keyGenerators = new HashMap<>();

    // 插件拦截器链
    protected final InterceptorChain interceptorChain = new InterceptorChain();

    // 类型别名注册机
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

    // 类型处理器注册机
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();

    // 对象工厂和对象包装器工厂
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

    protected final Set<String> loadedResources = new HashSet<>();
 
    //...
}

  • 单例模式:是一种创立型模式,让你可能保障一个类只有一个实例,并提供一个拜访该实例的全局节点。
  • 场景介绍:Configuration 就像狗皮膏药一样大单例,贯通整个会话的生命周期,所以的配置对象;映射、缓存、入参、出参、拦截器、注册机、对象工厂等,都在 Configuration 配置项中初始化。并随着 SqlSessionFactoryBuilder 构建阶段实现实例化操作。
  • 同类场景ErrorContextLogFactoryConfiguration

3. 建造者模式

源码详见cn.bugstack.mybatis.mapping.ResultMap#Builder

public class ResultMap {

    private String id;
    private Class<?> type;
    private List<ResultMapping> resultMappings;
    private Set<String> mappedColumns;

    private ResultMap() {}

    public static class Builder {private ResultMap resultMap = new ResultMap();

        public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings) {
            resultMap.id = id;
            resultMap.type = type;
            resultMap.resultMappings = resultMappings;
        }

        public ResultMap build() {resultMap.mappedColumns = new HashSet<>();
            // step-13 新减少,增加 mappedColumns 字段
            for (ResultMapping resultMapping : resultMap.resultMappings) {final String column = resultMapping.getColumn();
                if (column != null) {resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
                }
            }
            return resultMap;
        }

    }
    
    // ... get
}

  • 建造者模式:应用多个简略的对象一步一步构建成一个简单的对象,这种类型的设计模式属于创立型模式,它提供了一种创建对象的最佳形式。
  • 场景介绍:对于建造者模式在 Mybatis 框架里的应用,那真是纱窗擦屁股,给你漏了一手。到处都是 XxxxBuilder,所有对于 XML 文件的解析到各类对象的封装,都应用建造者以及建造者助手来实现对象的封装。它的外围目标就是不心愿把过多的对于对象的属性设置,写到其余业务流程中,而是用建造者的形式提供最佳的边界隔离。
  • 同类场景SqlSessionFactoryBuilderXMLConfigBuilderXMLMapperBuilderXMLStatementBuilderCacheBuilder

四、类型:结构型模式

1. 适配器模式

源码详见cn.bugstack.mybatis.logging.Log

public interface Log {boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);

}

源码详见cn.bugstack.mybatis.logging.slf4j.Slf4jImpl

public class Slf4jImpl implements Log {

  private Log log;

  public Slf4jImpl(String clazz) {Logger logger = LoggerFactory.getLogger(clazz);

    if (logger instanceof LocationAwareLogger) {
      try {
        // check for slf4j >= 1.6 method signature
        logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
        log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
        return;
      } catch (SecurityException e) {// fail-back to Slf4jLoggerImpl} catch (NoSuchMethodException e) {// fail-back to Slf4jLoggerImpl}
    }

    // Logger is not LocationAwareLogger or slf4j version < 1.6
    log = new Slf4jLoggerImpl(logger);
  }

  @Override
  public boolean isDebugEnabled() {return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {log.error(s, e);
  }

  @Override
  public void error(String s) {log.error(s);
  }

  @Override
  public void debug(String s) {log.debug(s);
  }

  @Override
  public void trace(String s) {log.trace(s);
  }

  @Override
  public void warn(String s) {log.warn(s);
  }

}

  • 适配器模式:是一种结构型设计模式,它能使接口不兼容的对象可能相互合作。
  • 场景介绍:正是因为有太多的日志框架,包含:Log4j、Log4j2、Slf4J 等等,而这些日志框架的应用接口又都各有差别,为了对立这些日志工具的接口,Mybatis 定义了一套对立的日志接口,为所有的其余日志工具接口做相应的适配操作。
  • 同类场景:次要集中在对日志的适配上,Log 和 对应的实现类,以及在 LogFactory 工厂办法中进行应用。

2. 代理模式

源码详见cn.bugstack.mybatis.binding.MapperProxy

public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;

    private SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);
        } else {final MapperMethod mapperMethod = cachedMapperMethod(method);
            return mapperMethod.execute(sqlSession, args);
        }
    }
    
    // ...

}

  • 代理模式:是一种结构型模式,让你可能提供对象的替代品或其占位符。代理管制着对原对象的拜访,并容许在将申请提交给对象前进行一些解决。
  • 场景介绍:不吹牛的讲,没有代理模式,就不会有各类的框架存在。就像 Mybatis 中的 MapperProxy 映射器代理实现类,它所实现的性能就是帮忙咱们实现 DAO 接口的具体实现类的办法操作,你的任何一个配置的 DAO 接口所调用的 CRUD 办法,都会被 MapperProxy 接管,调用到办法执行器等一系列操作,并返回最终的数据库执行后果。
  • 同类场景DriverProxyPluginInvokerMapperProxy

3. 组合模式

源码详见cn.bugstack.mybatis.scripting.xmltags.SqlNode

public interface SqlNode {boolean apply(DynamicContext context);

}

源码详见cn.bugstack.mybatis.scripting.xmltags.IfSqlNode

public class IfSqlNode implements SqlNode{

    private ExpressionEvaluator evaluator;
    private String test;
    private SqlNode contents;

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

    @Override
    public boolean apply(DynamicContext context) {
        // 如果满足条件,则 apply,并返回 true
        if (evaluator.evaluateBoolean(test, context.getBindings())) {contents.apply(context);
            return true;
        }
        return false;
    }

}

源码详见cn.bugstack.mybatis.scripting.xmltags.XMLScriptBuilder

public class XMLScriptBuilder extends BaseBuilder {private void initNodeHandlerMap() {
        // 9 种,实现其中 2 种 trim/where/set/foreach/if/choose/when/otherwise/bind
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("if", new IfHandler());
    }
 
    List<SqlNode> parseDynamicTags(Element element) {List<SqlNode> contents = new ArrayList<>();
        List<Node> children = element.content();
        for (Node child : children) {if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) {} else if (child.getNodeType() == Node.ELEMENT_NODE) {String nodeName = child.getName();
                NodeHandler handler = nodeHandlerMap.get(nodeName);
                if (handler == null) {throw new RuntimeException("Unknown element" + nodeName + "in SQL statement.");
                }
                handler.handleNode(element.element(child.getName()), contents);
                isDynamic = true;
            }
        }
        return contents;
    }
    
    // ...
}

配置详见resources/mapper/Activity_Mapper.xml

<select id="queryActivityById" parameterType="cn.bugstack.mybatis.test.po.Activity" resultMap="activityMap" flushCache="false" useCache="true">
    SELECT activity_id, activity_name, activity_desc, create_time, update_time
    FROM activity
    <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">
        <if test="null != activityId">
            activity_id = #{activityId}
        </if>
    </trim>
</select>

  • 组合模式:是一种结构型设计模式,你能够应用它将对象组合成树状构造,并且能独立应用对象一样应用它们。
  • 场景介绍 :在 Mybatis XML 动静的 SQL 配置中,共提供了 9 种(trim/where/set/foreach/if/choose/when/otherwise/bind) 标签的应用,让使用者能够组合出各类场景的 SQL 语句。而 SqlNode 接口的实现就是每一个组合构造中的规定节点,通过规定节点的组装实现一颗规定树组合模式的应用。具体应用源码能够浏览《手写 Mybatis:渐进式源码实际》
  • 同类场景:次要体现在对各类 SQL 标签的解析上,以实现 SqlNode 接口的各个子类为主。

4. 装璜器模式

源码详见cn.bugstack.mybatis.session.Configuration

public Executor newExecutor(Transaction transaction) {Executor executor = new SimpleExecutor(this, transaction);
    // 配置开启缓存,创立 CachingExecutor(默认就是有缓存)装璜者模式
    if (cacheEnabled) {executor = new CachingExecutor(executor);
    }
    return executor;
}

  • 装璜器模式:是一种结构型设计模式,容许你通过将对象放入蕴含行为的非凡封装对象中来为原对象绑定新的行为。
  • 场景介绍:Mybatis 的所有 SQL 操作,都是通过 SqlSession 会话调用 SimpleExecutor 简略实现的执行器实现的,而一级缓存的操作也是在简略执行器中解决。那么这里二级缓存因为是基于一级缓存刷新操作的,所以在实现上,通过创立一个缓存执行器,包装简略执行器的解决逻辑,实现二级缓存操作。那么这里用到的就是装璜器模式,也叫俄罗斯套娃模式。
  • 同类场景:次要提前在 Cache 缓存接口的实现和 CachingExecutor 执行器中。

五、类型:行为型模式

1. 模板模式

源码详见cn.bugstack.mybatis.executor.BaseExecutor

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {if (closed) {throw new RuntimeException("Executor was closed.");
    }
    // 清理部分缓存,查问堆栈为 0 则清理。queryStack 防止递归调用清理
    if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 依据 cacheKey 从 localCache 中查问数据
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list == null) {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {queryStack--;}
    if (queryStack == 0) {if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache();
        }
    }
    return list;
}

源码详见cn.bugstack.mybatis.executor.SimpleExecutor

protected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {Configuration configuration = ms.getConfiguration();
        // 新建一个 StatementHandler
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
        // 筹备语句
        stmt = prepareStatement(handler);
        // StatementHandler.update
        return handler.update(stmt);
    } finally {closeStatement(stmt);
    }
}

  • 模板模式:是一种行为设计模式,它在超类中定义了一个算法的框架,容许子类在不批改构造的状况下重写算法的特定步骤。
  • 场景介绍:只有存在一系列可被规范定义的流程,在流程的步骤大部分是通用逻辑,只有一少部分是须要子类实现的,那么通常会采纳模板模式来定义出这个规范的流程。就像 Mybatis 的 BaseExecutor 就是一个用于定义模板模式的抽象类,在这个类中把查问、批改的操作都定义出了一套规范的流程。
  • 同类场景BaseExecutorSimpleExecutorBaseTypeHandler

2. 策略模式

源码详见cn.bugstack.mybatis.type.TypeHandler

public interface TypeHandler<T> {

    /**
     * 设置参数
     */
    void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

    /**
     * 获取后果
     */
    T getResult(ResultSet rs, String columnName) throws SQLException;

    /**
     * 获得后果
     */
    T getResult(ResultSet rs, int columnIndex) throws SQLException;

}

源码详见cn.bugstack.mybatis.type.LongTypeHandler

public class LongTypeHandler extends BaseTypeHandler<Long> {

    @Override
    protected void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType) throws SQLException {ps.setLong(i, parameter);
    }

    @Override
    protected Long getNullableResult(ResultSet rs, String columnName) throws SQLException {return rs.getLong(columnName);
    }

    @Override
    public Long getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return rs.getLong(columnIndex);
    }

}

  • 策略模式:是一种行为设计模式,它能定义一系列算法,并将每种算法别离放入独立的类中,以使算法的对象可能相互替换。
  • 场景介绍:在 Mybatis 解决 JDBC 执行后返回的后果时,须要依照不同的类型获取对应的值,这样就能够防止大量的 if 判断。所以这里基于 TypeHandler 接口对每个参数类型别离做了本人的策略实现。
  • 同类场景PooledDataSource\UnpooledDataSourceBatchExecutor\ResuseExecutor\SimpleExector\CachingExecutorLongTypeHandler\StringTypeHandler\DateTypeHandler

3. 迭代器模式

源码详见cn.bugstack.mybatis.reflection.property.PropertyTokenizer

public class PropertyTokenizer implements Iterable<PropertyTokenizer>, Iterator<PropertyTokenizer> {public PropertyTokenizer(String fullname) {// 班级[0]. 学生. 问题
        // 找这个点 .
        int delim = fullname.indexOf('.');
        if (delim > -1) {name = fullname.substring(0, delim);
            children = fullname.substring(delim + 1);
        } else {
            // 找不到. 的话,取全副局部
            name = fullname;
            children = null;
        }
        indexedName = name;
        // 把中括号里的数字给解析进去
        delim = name.indexOf('[');
        if (delim > -1) {index = name.substring(delim + 1, name.length() - 1);
            name = name.substring(0, delim);
        }
    }

        // ...

}

  • 迭代器模式:是一种行为设计模式,让你能在不裸露汇合底层表现形式的状况下遍历汇合中所有的元素。
  • 场景介绍:PropertyTokenizer 是用于 Mybatis 框架 MetaObject 反射工具包下,用于解析对象关系的迭代操作。这个类在 Mybatis 框架中应用的十分频繁,包含解析数据源配置信息并填充到数据源类上,以及参数的解析、对象的设置都会应用到这个类。
  • 同类场景PropertyTokenizer

六、总结:“卷王”的心得

一份源码的成体系拆解渐进式学习,可能须要 1~2 个月的工夫,相比于爽文和疲于应试要花费更多的经验。但你总会在一个大块工夫学习完后,会在本人的头脑中构建出一套残缺体系对于此类常识的技术架构,无论从哪里入口你都能分明各个分支流程的走向,这也是你成为技术专家路上的深度学习。

如果你也想有这样酣畅淋漓的学习,千万别错过傅哥为你编写的材料《手写 Mybatis:渐进式源码实际》目录如图所示,共计 20 章

正文完
 0