简而言之,就是定义一个产品接口,定义一个工厂接口(包含生产产品的方法),每一个产品接口实现类都对应一个工厂接口的实现类去构造对应的产品实现类。
例如,Mybatis中的SqlSession接口和SqlSessionFactory接口,类图如下,这里的SqlSessionManager先暂时忽略。
图中的DefaultSqlSessionFactory就是生产DefaultSqlSession的工厂。
没错,忽略了SqlSessionManager之后,这俩接口都只有一个实现类,首先来看看这俩接口。
/**
* The primary Java interface for working with MyBatis.
* 这是使用Mybatis的主要Java接口
* Through this interface you can execute commands, get mappers and manage transactions.
* 通过这个接口,你可以执行命令,获取mappers(映射器)以及管理事务
*
* 接口里定义了sql语句对应的方法以及缓存、事务等方法
*
*/
public interface SqlSession extends Closeable {}
/**
* Creates an SqlSession out of a connection or a DataSource
* 基于连接或者数据源创建一个SqlSession
*
* SqlSession 的工厂接口,接口里定义了一系列openSession方法及获取配置方法。
*
*/
public interface SqlSessionFactory {}
两个接口的方法就不介绍了,毕竟也没啥好说。使用过Mybatis的小伙伴一定对下面这段代码十分熟悉。(摘自官方文档)
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//SqlSessionFactory的构建使用了建造者模式
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
} finally {
session.close();
}
这是单独使用Mybatis的基本用法,每次我们都使用工厂的openSession()来new一个DefaultSqlSession对象。所有openSession方法最终都是调用这两个方法。
/**
* 通过数据源来开启session
* @return new DefaultSqlSession(configuration, executor, autoCommit)
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {}
/**
* 通过数据库连接来开启session
* @return new DefaultSqlSession(configuration, executor, autoCommit);
*/
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {}
因此,在我们单独使用Mybatis的情况下,每次都是new一个新的session,虽然DefaultSqlSession是线程不安全的,但是我们单独使用是没问题的。
但是我们一般很少单独使用Mybatis,基本都是配合Spring这货一起用的。还记得下面这段配置吗?为什么我们需要这样配置呢?
首先,Spring中的bean默认是单例的,但是呢DefaultSqlSession又是非线程安全的,那么如果将Default-SqlSession直接交由Spring管理,我把它将配成多例是不是就可以了呢?
理论上好像是没问题的,但是每次数据库操作都去new一个对象未免太损耗性能了。(这是我觉得不能直接使用DefaultSqlSession的一个原因 )。而且若是每次使用都new一个对象的话,那么事务交由Spring管理的时候,那么事务就无法生效了。因此,就有了上面一段配置了。
SqlSessionTemplate也实现了SqlSession接口,而且在类注释的第一句就说明了他是线程安全的,那么,他又是如何实现线程安全的呢?
成员变量
//Session工厂
private final SqlSessionFactory sqlSessionFactory;
//执行的类型 有simple,batch,reuse
private final ExecutorType executorType;
//根据命名看出这是一个代理的SqlSession对象,重点就在于这个代理对象啦
private final SqlSession sqlSessionProxy;
// 根据名字翻译为持久化异常翻译器
private final PersistenceExceptionTranslator exceptionTranslator;
接着来看构造函数
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
//生成代理类,表示SqlSession这个借口的方法都被SqlSessionInterceptor拦截。
//这里就是代理模式的体现啦
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
那么SqlSessionInterceptor又做了些什么呢
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这个方法就是关键,获取session,
//它大概的流程就是若是在spring的事务管理之中,会共用一个SqlSession,将引用次数加1
//否则每次执行方法都是new一个新的SqlSession
//具体实现则是通过ThreadLocal来实现
//比如,我们直接在controller中调用dao,每次调用都会产生一个新的SqlSession对象
//而在service中加了事务的方法中多次调用dao的方法,则是共用一个SqlSession对象
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
// 因此,最终执行方法还是通过DefaultSqlSession来执行的
Object result = method.invoke(sqlSession, args);
//若不是spring事务管理的
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
//强制提交
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
......
} finally {
if (sqlSession != null) {
//若是事务交由spring管理则释放引用次数,即减1,
//否则直接关闭
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
现在再去看SqlSessionManager这个类,这个类除了一个作者之外再无其他注释,所以无法直接看出他的作用,他实现SqlSession和SqlSessionFactory这两个接口,而且他也使用了类似SqlSessionTemplate的代理模式,代码就不贴了,并且一样的利用了ThreadLocal来提供了线程安全的SqlSession。
个人猜测,这个类是出于在单独使用Mybtais时候能够让使用者方便地构造线程安全的SqlSession的目的而提供的。
关于代理模式,个人认为和其他设计模式是有一个很大的区别的 —— 其他的设计模式主要侧重于接口和类的设计与类与类之间的配合使用,也可以说是侧重于设计; 而代理模式则更侧重于开发实现,换言之,学习代理模式更倾向于学习具体的实现和使用方式,因为代理模式依赖于语言特性或者说是编程语言本身提供的相关接口(动态代理)。
归根结底,SqlSessionTemplate中保证线程安全主要还是依赖于ThreadLocal来实现,那么,为什么是将一个SqlSession绑定到一个事务中呢?比如一个controller调用了service层的三个加了事务的方法,那么完成这个请求则需要新建三个SqlSession,为什么Spring不在一个请求中共用一个SqlSession呢,这样性能岂不是更好?
我个人的理解是如果一个请求都共用一个SqlSession,那么close这个session的任务就归属到了事务方法的调用方,Web开发一般就是controller层,而controller层的职责应该是请求的接收和返回对应的数据,若是把close交由controller层处理,导致controller承担了本不属于他的责任,这样就违背了我们分层的初衷了。
原文链接:https://www.yuque.com/jinse95/develop/emihae