今天特意来了一篇设计模式的实战,跟着源码真正了解设计模式,因为我第一次学习设计模式的时候,都是只知道每一个具体的设计模式的意思,并没有了解到一些框架优秀的设计模式!今天特意写一下这个
总结Mybatis框架用到的设计模式
SqlSessionFactoryBuilder:为什么要用建造者模式来创建SqlSessionFactory?
简单谈谈建造者模式
这里主要说说为什么需要建造者模式?
建造者模式和工厂模式都是用来创建对象的。平常我们一般创建对象都是直接new,通过构造器或者setter把对象属性注入。那如果这时候我们的属性变量很多,额,这里直接举个例子吧!
我们可以看看上面的需求,我们可以这样写
public class ResourceConfig {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 8;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourceConfig(String name) {
this.name = name;
}
// 省略getter和setter
// 可以利用setter来给不是必填的变量赋值
}
上面这样写再构造参数不多的时候是可以这样写到的,那如果必填的构造函数很多呢?那这样做就不行了,因为很容易导致传参错位,从而导致程序出错。或者是变量与变量之间有一些校验逻辑关系,那也会造成构造函数臃肿
还有一种情况是,如果这个类是一个不可变类,那它的setter方法就不能向外提供了,所以上面这样写也是不满足的!
那这时候就需要建造者模式了
public class ResourceConfigBuilder {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourceConfigBuilder(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxTotal;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
@Override
public String toString() {
return "ResourceConfigBuilder{" +
"name='" + name + '\'' +
", maxTotal=" + maxTotal +
", maxIdle=" + maxIdle +
", minIdle=" + minIdle +
'}';
}
// 省略getter
public static class Builder{
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 8;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
private String name;
public ResourceConfigBuilder build(){
// 校验逻辑放到这里
// if.....
if (maxIdle > maxTotal){
throw new IllegalArgumentException("test");
}
return new ResourceConfigBuilder(this);
}
public Builder setMaxTotal(int maxTotal) {
this.maxTotal = maxTotal;
return this;
}
public Builder setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
return this;
}
public Builder setMinIdle(int minIdle) {
this.minIdle = minIdle;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
}
public static void main(String[] args) {
ResourceConfigBuilder resourceConfigBuilder = new Builder()
.setName("professor")
.setMaxIdle(200)
.setMinIdle(100)
.setMaxTotal(400)
.build();
System.out.println(resourceConfigBuilder.toString());
}
}
结果也如我们所料设置成功,并且该方法的构造函数是私有的,并且没有向外提供setter方法,同时满足了上面我说过的条件!!
那我们试试校验逻辑是否可以校验成功,也是成功的
进入主题
// 可以先看看这段代码,是进行Mybatis编程的时候必写的
private SqlSessionFactory factory;
@Before
public void setUp() throws Exception {
// 这里用到了SqlSessionFactoryBuilder,看名字,你可能以为这是建造者模式来创建SqlSessionFactory对象,我们先看看源码
factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
}
// 我们可以看到这里根本没有setter方法,而且 build() 方法也并非无参,需要传递参数,这明显不符合建造者模式的套路啊!!
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) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
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) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
// 最终所有的构造函数都会调用这个方法
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
// 再来看看SqlSessionFactory
// 这个方法的成员变量只有一个,那为什么不直接用构造函数来呢?为什么还要借助建造者模式创建 SqlSessionFactory呢?
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
}
//
好了,这里揭开上面的问题,我们debug进去看看,我们先进入这里
不妨进入parse方法看看,这里调用的是parseConfiguration
这个方法,我们就知道大概原因了,因为这个Mybatis的这个Configuration
要配置很多的成员变量才能成功创建,那这个SqlSessionFactoryBuilder
就是简化我们的开发的,将过程隐藏起来,为Configuration
对象的构建省略一些无关的逻辑!
这里不妨再看看Configuration
类
SqlSessionFactory:到底属于工厂模式还是建造器模式?
浅谈工厂模式
工厂模式也是创建对象的一种设计模式
工厂模式又分为简单工厂、工厂方法和抽象工厂,这里就细谈一下简单工厂和工厂方法
简单工厂
举个例子:比如现在的业务是根据不同的文件后缀名选择不同的解析器
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath){
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParser parser = null;
// 可以看到这一块逻辑很累赘
if ("json".equalsIgnoreCase(ruleConfigFileExtension)){
parser = new JsonRuleConfigParser();
}else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)){
parser = new XmlRuleConfigParser();
}else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)){
parser = new YamlRuleConfigParser();
}else {
throw new RuntimeException("test error");
}
String configText = "";
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String ruleConfigFilePath) {
return null;
}
}
// 可以用简单工厂模式实现上面那个创建对象的功能
public class RuleConfigParserFactory {
public static IRuleConfigParser createParser(String configFormat){
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(configFormat)){
parser = new JsonRuleConfigParser();
}else if ("xml".equalsIgnoreCase(configFormat)){
parser = new XmlRuleConfigParser();
}else if ("yaml".equalsIgnoreCase(configFormat)){
parser = new YamlRuleConfigParser();
}else {
throw new RuntimeException("test error");
}
return parser;
}
}
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath){
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
// 直接调用工厂方法
IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
String configText = "";
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String ruleConfigFilePath) {
return null;
}
}
// 上面只是第一种方式,这种方式每次调用RuleConfigParserFactory.createParser都要新建一个对象,我们要不要结合一下单例模式,可以将 parser 事先创建好缓存起来。当调用 createParser() 函数的时候,我们从缓存中取出 parser 对象直接使用。
// 简单工厂模式的第二种实现
public class RuleConfigParserFactory {
private static final Map<String, IRuleConfigParser> cachedParsers = new HashMap<String, IRuleConfigParser>();
static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
cachedParsers.put("yaml", new YamlRuleConfigParser());
}
public static IRuleConfigParser createParser(String configFormat){
if (configFormat == null){
return null;
}
IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
return parser;
}
}
// 上面这种方法虽然违反了开闭原则,但是修改的代码不多还是可以接受的
工厂方法
public interface IRuleConfigParserFactory {
public RuleConfig parse(String configText);
}
public class JsonRuleConfigParser implements IRuleConfigParserFactory {
public RuleConfig parse(String configText) {
// 自己的逻辑
return null;
}
}
public class XmlRuleConfigParser implements IRuleConfigParserFactory {
public RuleConfig parse(String configText) {
// 自己的逻辑
return null;
}
}
public class YamlRuleConfigParser implements IRuleConfigParserFactory {
public RuleConfig parse(String configText) {
// 自己的逻辑
return null;
}
}
// 上面这种工厂方法更符合开闭原则
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath){
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParserFactory parser = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
String configText = "";
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String ruleConfigFilePath) {
return null;
}
}
// 工厂的工厂
// 当要增加新的parser类的时候,只需要实现该类的逻辑,并在下面这个类改一下static的内容即可
public class RuleConfigParserFactoryMap {
private static final Map<String, IRuleConfigParserFactory> cachedParsers = new HashMap<String, IRuleConfigParserFactory>();
static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
cachedParsers.put("yaml", new YamlRuleConfigParser());
}
public static IRuleConfigParserFactory getParserFactory(String type){
if (type == null){
return null;
}
IRuleConfigParserFactory parser = cachedParsers.get(type.toLowerCase());
return parser;
}
}
那什么时候该用工厂方法模式,而非简单工厂模式呢?
- 当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。
- 如果我们还想避免烦人的 if-else 分支逻辑,这个时候,我们就推荐使用工厂方法模式。
回到主题
可以先看看DefaultSqlSessionFactory
的源码,
BaseExecutor:模板模式跟普通的继承有什么区别?
模板模式和继承,这里就不细说了。模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用有待子类实
现的抽象方法,那这一般就是模板模式的代码实现。而且,在命名上,模板方法与抽象方法一般是一一对应的,抽象方法在模板方法前面多一个“do”,比如,在 BaseExecutor 类中,其中一个模板方法叫 update(),那对应的抽象方法就叫 doUpdate()。
可以看看BaseExecutor
的源码
再来看看SimpleExecutor
SqlNode:如何利用解释器模式来解析动态 SQL?
关于解析动态SQL,我上篇文章也有简单提了一下。
简单谈谈解析器模式
它用来描述如何构建一个简单的“语言”解释器,说白了,其实就是一个翻译器,好像百度翻译,将输入的中文翻译成英文。这里的翻译器就是解释器模式定义中的“解释器”。其实这种设计模式用得不多,所以就不重点介绍了!
回到正题
拿什么是动态SQL呢?就是在SQL 中可以包含在 trim、if、#{}等语法标签,在运行时根据条件来生成不同的 SQL。解释器模式在解释语法规则的时候,一般会把规则分割成小的单元,特别是可以嵌套的小单元,针对每个小单元来解析,最终再把解析结果合并在一起。这里也不例外。MyBatis 把每个语法小单元叫 SqlNode,这样就可以形成一颗SqlNode树
// 这个代码很简单
public interface SqlNode {
boolean apply(DynamicContext context);
}
每一个节点都会解析不同的语法,这里就不细讲了
ErrorContext:如何实现一个线程唯一的单例模式?
在 MyBatis 中,ErrorContext 这个类就是标准单例的变形:线程唯一的单例。
Cache:为什么要用装饰器模式而不设计成继承子类
谈谈装饰器模式
装饰器模式其实跟继承是很相似的。下面我们举一个JAVA IO的例子,因为java的IO设计就是用的装饰器模式
-
装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
// 可以看看这段代码理解一下 // 让它既支持缓存读取,又支持按照基本数据类型来读取数据 InputStream in = new FileInputStream("test.txt"); InputStream bin = new BufferedInputStream(in); DataInputStream din = new DataInputStream(bin); int data = din.readInt();
那如果不用装饰器模式,用继承来实现这个增强功能的话可以用一个类搞定
// InputStream din = new FileBufferedDataInputStream("test.txt"); int data = din.readInt();
像上面那样,如果用继承的话,看起来确实简洁了,但是内部实现确非常复杂,我们置到InputStream有很多子类继承,我们不可能让每个子类都实现都继承一些我们想要的功能吧,这样做的话真的是要做很多个组合类才行!!所以,用继承来实现显然是不妥的。
-
装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。其实它只是把用继承实现的增强换成了用组合模式来实现
回到主题
如果对Mybatis缓存不太了解的话,可以先看看这篇文章。以LruCache为例分析一下
public class LruCache implements Cache {
// 以组合的方式代替继承
private final Cache delegate;
// 缓存Map
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
// 增强方法
keyMap.get(key); //touch
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
可以看到上面就是典型的装饰器模式的实现。
Log:如何使用适配器模式来适配不同的日志框架?
谈谈适配器模式
这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。我们可以举个例子
// 类适配器: 基于继承
public interface ITarget {
void f1();
void f2();
void fc();
}
// Adaptee 是一组不兼容 ITarget接口定义的接口
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
// 通过一个继承+接口的方法实现将 Adaptee 转化成一组符合 ITarget 接口定义的接口
public class Adaptor extends Adaptee implements ITarget {
public void f1() {
super.fa();
}
public void f2() {
//...重新实现f2()...
}
// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
// 还有一种方法
// 对象适配器:基于组合
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor implements ITarget {
private Adaptee adaptee;
public Adaptor(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void f1() {
adaptee.fa(); //委托给Adaptee
}
public void f2() {
//...重新实现f2()...
}
public void fc() {
adaptee.fc();
}
}
- 如果 Adaptee 接口并不多,那两种实现方式都可以。
- 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那我们推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
- 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。
回到正题
MyBatis 并没有直接使用 Slf4j提供的统一日志规范,而是自己又重复造轮子,定义了一套自己的日志访问接口。
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);
}
我们看看Log4jImpl
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
// 用的组合方式实现适配器
private Logger log;
public Log4jImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
@Override
public void error(String s) {
log.log(FQCN, Level.ERROR, s, null);
}
@Override
public void debug(String s) {
log.log(FQCN, Level.DEBUG, s, null);
}
@Override
public void trace(String s) {
log.log(FQCN, Level.TRACE, s, null);
}
@Override
public void warn(String s) {
log.log(FQCN, Level.WARN, s, null);
}
}
利用职责链与代理模式实现Mybatis插件机制
谈谈职责链模式
在职责链模式中,多个处理器(也就是刚刚定义中说的“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。(这里有点像Netty的PipleLine种的handler,每一个handler都有自己的职责,昨晚自己的任务就通过handlerContext传递给下一个handler处理下一个任务)
我们来以Netty来举个例子吧,在这篇文章谈到过Netty的使用,不熟悉Netty的可以先看看。
谈谈代理模式
它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。最典型的实现就是Spring-AOP了,不了解的朋友可以看看我之前的文章
重点说说动态代理的原理,就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后
在系统中用代理类替换掉原始类。直接看看代码实现吧
// 被代理类
public interface IVehical {
void run();
}
public class Car implements IVehical {
public void run() {
System.out.println("Car会跑");
}
}
// 动态代理类
public class DynamicProxyHandler implements InvocationHandler {
private Object proxiedObject;
public DynamicProxyHandler(Object proxiedObject) {
this.proxiedObject = proxiedObject;
}
/**
* 执行代理类逻辑
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTimeStamp = System.currentTimeMillis();
// 调用被代理类
Object result = method.invoke(proxiedObject, args);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimeStamp;
System.out.println("该接口调用时间:"+ responseTime);
String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
System.out.println("接口名字:" + apiName);
return result;
}
}
// 测试
public static void main(String[] args) {
IVehical car = new Car();
// 创建代理类与被代理类的关系
// newProxyInstance
IVehical iVehical = (IVehical) Proxy.newProxyInstance(car.getClass().getClassLoader(), Car.class.getInterfaces(), new DynamicProxyHandler(car));
// 这里调用被代理类的时候就可以先调用代理类的逻辑了
iVehical.run();
}
个人唠叨
嘻嘻,先报告一下今天的算法学习情况,一切良好,两个多小时,题量应该有4或者5题这样子,已经开始有感觉了(虽然还未能free bug,但是已经可以写到完整的代码,思路也十分清晰了,只是细节方面不太注意!)有进步,哈哈哈,还是要多刷题啊,最近刚认识了一位师兄,他说都刷了两年了,每天如此
大佬不愧是大佬哎!!要多学习学习
谈谈最近的状态
最近的状态有点飘,主要是因为努力的方向出了一点点小问题,徘徊在应该怎么复习基础知识上,于是我就把问题抛上了知识星球和老师,看看大佬们都怎么回答的!!问题是这样子的
看看大佬是怎么回复我的
然后,师兄也是这么说的,首先上来也夸了我一下,还是有点飘了!!然后说我算法比骄傲薄弱(确实)!所以之后重点还是在算法,至于基础知识的复习,还是通过面试题的方式或者是看源码的方式复习到!!后期会出具体的文章,我是怎么复习基础知识的,敬请关注!!
好了,今天就说这么多吧!!晚安!!!
对于上面我提到的问题,有想法的大佬欢迎评论区给我留言,感谢感谢!!