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学习所有总结的一些设计模式的使用,后续学习会继续跟进。