MyBatis之设计模式学习总结

2023-03-01 13:54:36 浏览数 (1)

MyBatis之设计模式学习总结

学习Mybatis学习,不仅要学习它的设计思想,还要学习其中设计模式的运用。下面是记录一下在学习过程中了解的设计模式的运用。 结合所学目前只总结了7种设计模式,后续会一直补充。

1、建造者模式(Builder模式)

Builder模式的定义是“将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。”,它属于创建类模式。

举个例子:

代码语言:javascript复制
public class Book {

    private Integer id;
    private String name;

    public Book() {
    }

    public Book(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Book{"  
                "id="   id  
                ", name='"   name  
                '}';
    }
    // 通过内部类的方式,使用建造者模式
    public static class Builder{
        private Integer id;
        private String name;

        public Builder id(Integer id) {
            this.id = id;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }
        public Book build(){
            return new Book(id, name);
        }
    }
}

/**
 * 这样就实现了链接创建方式,和Lombok的@Builder注解的作用,如出一辙。
 */
public class BuilderTest {

    public static void main(String[] args) {
        Book book = new Book.Builder().id(1).name("三国").build();
        System.out.println(book.toString());
    }
}

Mybatis中Builder模式的使用则不局限于内部类方式的使用。

例如:

代码语言:javascript复制
/**
 * 根据不同的输入参数来构建SqlSessionFactory这个工厂对象。
 * 最后会执行这个build方法,创建SqlSessionFactory的实现类
 */
public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(Reader reader) {
        return build(reader, null, null);
    }

    public SqlSessionFactory build(Reader reader, String environment) {
        return build(reader, environment, null);
    }

    public SqlSessionFactory build(Reader reader, Properties properties) {
        return build(reader, null, properties);
    }

    public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        return build(parser.parse());
    }

    
    public SqlSessionFactory build(InputStream inputStream) {
        return build(inputStream, null, null);
    }

    public SqlSessionFactory build(InputStream inputStream, String environment) {
        return build(inputStream, environment, null);
    }

    public SqlSessionFactory build(InputStream inputStream, Properties properties) {
        return build(inputStream, null, properties);
    }

    public SqlSessionFactory build(InputStream inputStream, String environment, 
                                   Properties properties) {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    }

    // 最后会执行这个build方法,创建SqlSessionFactory的实现类
    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
}

以上是Mybatis使用建造者模式的使用场景之一。

2、工厂模式

Mybatis中使用较为简单的工厂模式,比如有SqlSessionFactory,没有复杂的逻辑,是一个简单工厂模式。

简单工厂模式(Simple Factory Pattern),也可以称为静态工厂方法(Static Factory Method),属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。

举个简单例子:

代码语言:javascript复制
/**
 * 消息接口
 */
public interface IMessage {
    void sayHello(String name);
}

/**
 * QQ方式实现sayHello
 */
public class QqMessage implements IMessage{

    @Override
    public void sayHello(String name) {
        System.out.println("QQ方式: "   name   " say hello!");
    }
}

/**
 * 微信方式实现sayHello
 */
public class WeChatMessage implements IMessage{

    @Override
    public void sayHello(String name) {
        System.out.println("微信方式: "   name   " say hello!");
    }
}

/**
 * 简单工厂
 */
public class MessageFactory {

    /**
     * 通过类型获取工厂类实例
     * @param type 工厂创建类型
     * @return IMessage
     */
    public static IMessage createSendMessage(String type) {
        IMessage iMessage = null;
        if ( "QQ".equals(type) ) {
            iMessage = new QqMessage();
        } else if ("WeChat".equals(type)){
            iMessage = new WeChatMessage();
        } else {
            throw new NullPointerException();
        }
        return iMessage;
    }
}

/**
 * 简单工厂实现测试
 */
public class FactoryTest {

    public static void main(String[] args) {
        IMessage message = MessageFactory.createSendMessage("QQ");
        message.sayHello("工厂创建");
    }
}

这是基本的工厂实现逻辑,根据传入的参数,工厂进行复杂逻辑处理后,创建接口的实现类。

那么Mybatis中的简单工厂模式简单案例如下:

代码语言:javascript复制
/**
 * 调用工厂的openSession方法,创建SqlSession的默认实现类 -DefaultSqlSession
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    @Override
    public SqlSession openSession() {
        return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
    }
    
    private SqlSession openSessionFromDataSource(ExecutorType execType, 
                                      TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
            // 1、复杂的逻辑处理
            final Environment environment = configuration.getEnvironment();
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            final Executor executor = configuration.newExecutor(tx, execType);
            // 2、创建SqlSession的实现类
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
            closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session.  Cause: "   e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
}
3、单例模式

单例模式是指:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

举个例子:

代码语言:javascript复制
public class Singleton {
    public static Singleton instance = new Singleton();
    // 禁止实例化
    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}

在Mybatis中有两个地方用到单例模式:ErrorContext和LogFactory。

1、ErrorContext 是在线程范围内的单例,用于记录该线程的执行环境错误信息。

代码语言:javascript复制
public class ErrorContext {

    private static final String LINE_SEPARATOR = System.lineSeparator();
    private static final ThreadLocal<ErrorContext> LOCAL =
         					ThreadLocal.withInitial(ErrorContext::new);
	private ErrorContext stored;
    
    // 获取线程中 ErrorContext 实例
    public static ErrorContext instance() {
        return LOCAL.get();
    }

    public ErrorContext store() {
        ErrorContext newContext = new ErrorContext();
        newContext.stored = this;
        LOCAL.set(newContext);
        return LOCAL.get();
    }

    public ErrorContext recall() {
        if (stored != null) {
            LOCAL.set(stored);
            stored = null;
        }
        return LOCAL.get();
    }
}

2、LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。

代码语言:javascript复制
/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public final class LogFactory {

    /**
     * Marker to be used by logging implementations that support markers.
     */
    public static final String MARKER = "MYBATIS";

    private static Constructor<? extends Log> logConstructor;

    static {
        /**
         * 不理解lambda表达式,可以如下理解,写法不同而已:
         * tryImplementation(new Runnable() {
         *     @Override
         *     public void run() {
         *         useSlf4jLogging();
         *     }
         * });
         */
        tryImplementation(LogFactory::useSlf4jLogging);
        tryImplementation(LogFactory::useCommonsLogging);
        tryImplementation(LogFactory::useLog4J2Logging);
        tryImplementation(LogFactory::useLog4JLogging);
        tryImplementation(LogFactory::useJdkLogging);
        tryImplementation(LogFactory::useNoLogging);
    }

    private LogFactory() {
        // disable construction
    }

    public static Log getLog(Class<?> clazz) {
        return getLog(clazz.getName());
    }

    public static Log getLog(String logger) {
        try {
            return logConstructor.newInstance(logger);
        } catch (Throwable t) {
            throw new LogException("Error creating logger for logger "   logger   ".  
                                   Cause: "   t, t);
        }
    }

    public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
        setImplementation(clazz);
    }

    public static synchronized void useSlf4jLogging() {
        setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
    }

    public static synchronized void useCommonsLogging() {
        setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
    }

    public static synchronized void useLog4JLogging() {
        setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
    }

    public static synchronized void useLog4J2Logging() {
        setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
    }

    public static synchronized void useJdkLogging() {
        setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
    }

    public static synchronized void useStdOutLogging() {
        setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
    }

    public static synchronized void useNoLogging() {
        setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
    }

    /**
     * 循环匹配系统所使用的日志框架,及日志框架的单一的,在类中即为单例的
     * 如果日志接口为null,则设置,如果不为null则跳过。Mybatis系统只会使用一个日志框架。
     */
    private static void tryImplementation(Runnable runnable) {
        if (logConstructor == null) {
            try {
                runnable.run();
            } catch (Throwable t) {
                // ignore
            }
        }
    }

    /**
     * 根据传入的日志框架实现类实例化Log对象
     */
    private static void setImplementation(Class<? extends Log> implClass) {
        try {
            
            Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
            Log log = candidate.newInstance(LogFactory.class.getName());
            if (log.isDebugEnabled()) {
                log.debug("Logging initialized using '"   implClass   "' adapter.");
            }
            logConstructor = candidate;
        } catch (Throwable t) {
            throw new LogException("Error setting Log implementation.  Cause: "   t, t);
        }
    }

}
4、代理模式

MyBatis框架通过使用动态代理模式,使得只需要编写接口类(Mapper接口),就可以实现具体的底层SQL实现。

下面举个简单动态代理实现的例子:

代码语言:javascript复制
/**
 * mapper接口
 */
public interface BookMapper {
    // 有返回值
    int selectById(Integer id);
    // 无返回值
    void insert(Integer id);
}

/**
 * mapper接口动态代理工厂
 */
public class MapperFactory {

    public <T> T getMapper(Class<?> mapperClass){
        /** 如何下面无法理解,则可以参考这个代理实现,写法不同,但是逻辑相同。
         * Proxy.newProxyInstance(MapperFactory.class.getClassLoader(), new Class[] 	{mapperClass}, new InvocationHandler() {
         *     @Override
         *     public Object invoke(Object proxy, Method method, Object[] args) 
         *												             throws Throwable {
         *          return null;
         *     }
         * });
         */
        Object proxy = Proxy.newProxyInstance(MapperFactory.class.getClassLoader(), 
        								new Class[]{mapperClass}, (o, method, args) -> {
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();
            String statementId = className   "."   methodName;
            // com.tjau.proxy.BookMapper.selectById
            // 或者 com.tjau.proxy.BookMapper.insert
            System.out.println(statementId);
            // 模拟逻辑处理
            if ( "selectById".equals(methodName) ) {
                return args[0];
            } else {
                return null;
            }
        });
        return (T) proxy;
    }
}

/**
 * 测试类
 */
public class ProxyTest {

    public static void main(String[] args) {
        MapperFactory factory = new MapperFactory();
        BookMapper mapper = factory.getMapper(BookMapper.class);
        System.out.println(mapper.selectById(1001));
        System.out.println("=======================");
        mapper.insert(1102);
    }
}
/** 打印结果
 *  com.tjau.proxy.BookMapper.selectById
 *  1001
 *  =======================
 *  com.tjau.proxy.BookMapper.insert
 */

通过以上案例,可见动态代理的便捷之处。

那么可再看MyBatis中的实现逻辑(这里只粘贴部分重要)。

代码语言:javascript复制
public class MapperRegistry {

    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

    @SuppressWarnings("unchecked")
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>)knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type "   type   " is not"  
                                       " known to the MapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: "   e, e);
        }
    }
}

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

    /**
     * 第一步
     */
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
    
    /**
     * 第二步:这里发现了熟悉的关键字
     *        至于为什么mapperProxy是显示类。则需要看其结构实现。
     */
    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }
}

/**
 * MapperProxy类实现结构
 */
public class MapperProxy<T> implements InvocationHandler{}
5、组合模式和策略模式

组合模式:组合多个对象形成树形结构以表示" 整体-部分 "的结构层次。 策略模式:定义了一组算法,把使用算法的责任和实现分割开来,并委派给不同的对象对这些算法进行管理。

从Map层面,可以认为时策略模式,根据XML中不同的SQL标签,委派为不同的对象来处理。

从实现类包含层面,策略中又存在包含关系,则又符合组合模式中的 “ 整体 - 部分 ”的概念。比如 标签中包含标签等。

组合模式的简单之处在于,所有的类都实现同一个接口,因此可以递归的向下执行。

代码语言:javascript复制
public class XMLScriptBuilder extends BaseBuilder {

    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
	
    private void initNodeHandlerMap() {
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }
    
}
6、组合模式

模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。

在Mybatis中,sqlSession的SQL执行,都是委托给Executor实现的,Executor包含以下结构:

其中的BaseExecutor就采用了模板方法模式,它实现了大部分的SQL执行逻辑,然后把以下几个方法交给子类定制化完成:

代码语言:javascript复制
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;

protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, 
ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

该模板方法类有几个子类的具体实现,使用了不同的策略:

  • 简单SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。(可以是Statement或PrepareStatement对象)
  • 重用ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。(可以是Statement或PrepareStatement对象)
  • 批量BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或PrepareStatement对象)
7、适配器模式

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

以MyBatis中的日志框架为例:

代码语言:javascript复制
/**
 * 日志接口
 */
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);
}

/**
 * 类似于包装类,委托Logger的实现类来实现日志打印
 */
public class Slf4jLoggerImpl implements Log {

    private final Logger log;

    public Slf4jLoggerImpl(Logger logger) {
        log = 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);
    }
}

MyBatis编写所有日志框架的包装类是适配所有的日志框架。

以上是目前MyBatis学习所有总结的一些设计模式的使用,后续学习会继续跟进。

0 人点赞