This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,359 @@
<audio id="audio" title="76 | 开源实战一通过剖析Java JDK源码学习灵活应用设计模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ba/31/bac570414d9984d28fea9e761fa7f631.mp3"></audio>
从今天开始,我们就正式地进入到实战环节。实战环节包括两部分,一部分是开源项目实战,另一部分是项目实战。
在开源项目实战部分我会带你剖析几个经典的开源项目中用到的设计原则、思想和模式这其中就包括对Java JDK、Unix、Google Guava、Spring、MyBatis这样五个开源项目的分析。在项目实战部分我们精心挑选了几个实战项目手把手地带你利用之前学过的设计原则、思想、模式来对它们进行分析、设计和代码实现这其中就包括鉴权限流、幂等重试、灰度发布这样三个项目。
接下来的两节课我们重点剖析Java JDK中用到的几种常见的设计模式。学习的目的是让你体会在真实的项目开发中要学会活学活用切不可过于死板生搬硬套设计模式的设计与实现。除此之外针对每个模式我们不可能像前面学习理论知识那样分析得细致入微很多都是点到为止。在已经具备之前理论知识的前提下我想你可以跟着我的指引自己去研究有哪里不懂的话也可以再回过头去看下之前的理论讲解。
话不多说,让我们正式开始今天的学习吧!
## 工厂模式在Calendar类中的应用
在前面讲到工厂模式的时候大部分工厂类都是以Factory作为后缀来命名并且工厂类主要负责创建对象这样一件事情。但在实际的项目开发中工厂类的设计更加灵活。那我们就来看下工厂模式在Java JDK中的一个应用java.util.Calendar。从命名上我们无法看出它是一个工厂类。
Calendar类提供了大量跟日期相关的功能代码同时又提供了一个getInstance()工厂方法用来根据不同的TimeZone和Locale创建不同的Calendar子类对象。也就是说功能代码和工厂方法代码耦合在了一个类中。所以即便我们去查看它的源码如果不细心的话也很难发现它用到了工厂模式。同时因为它不单单是一个工厂类所以它并没有以Factory作为后缀来命名。
Calendar类的相关代码如下所示大部分代码都已经省略我只给出了getInstance()工厂方法的代码实现。从代码中我们可以看出getInstance()方法可以根据不同TimeZone和Locale创建不同的Calendar子类对象比如BuddhistCalendar、JapaneseImperialCalendar、GregorianCalendar这些细节完全封装在工厂方法中使用者只需要传递当前的时区和地址就能够获得一个Calendar类对象来使用而获得的对象具体是哪个Calendar子类的对象使用者在使用的时候并不关心。
```
public abstract class Calendar implements Serializable, Cloneable, Comparable&lt;Calendar&gt; {
//...
public static Calendar getInstance(TimeZone zone, Locale aLocale){
return createCalendar(zone, aLocale);
}
private static Calendar createCalendar(TimeZone zone,Locale aLocale) {
CalendarProvider provider = LocaleProviderAdapter.getAdapter(
CalendarProvider.class, aLocale).getCalendarProvider();
if (provider != null) {
try {
return provider.getInstance(zone, aLocale);
} catch (IllegalArgumentException iae) {
// fall back to the default instantiation
}
}
Calendar cal = null;
if (aLocale.hasExtensions()) {
String caltype = aLocale.getUnicodeLocaleType(&quot;ca&quot;);
if (caltype != null) {
switch (caltype) {
case &quot;buddhist&quot;:
cal = new BuddhistCalendar(zone, aLocale);
break;
case &quot;japanese&quot;:
cal = new JapaneseImperialCalendar(zone, aLocale);
break;
case &quot;gregory&quot;:
cal = new GregorianCalendar(zone, aLocale);
break;
}
}
}
if (cal == null) {
if (aLocale.getLanguage() == &quot;th&quot; &amp;&amp; aLocale.getCountry() == &quot;TH&quot;) {
cal = new BuddhistCalendar(zone, aLocale);
} else if (aLocale.getVariant() == &quot;JP&quot; &amp;&amp; aLocale.getLanguage() == &quot;ja&quot; &amp;&amp; aLocale.getCountry() == &quot;JP&quot;) {
cal = new JapaneseImperialCalendar(zone, aLocale);
} else {
cal = new GregorianCalendar(zone, aLocale);
}
}
return cal;
}
//...
}
```
## 建造者模式在Calendar类中的应用
还是刚刚的Calendar类它不仅仅用到了工厂模式还用到了建造者模式。我们知道建造者模式有两种实现方法一种是单独定义一个Builder类另一种是将Builder实现为原始类的内部类。Calendar就采用了第二种实现思路。我们先来看代码再讲解相关代码我贴在了下面。
```
public abstract class Calendar implements Serializable, Cloneable, Comparable&lt;Calendar&gt; {
//...
public static class Builder {
private static final int NFIELDS = FIELD_COUNT + 1;
private static final int WEEK_YEAR = FIELD_COUNT;
private long instant;
private int[] fields;
private int nextStamp;
private int maxFieldIndex;
private String type;
private TimeZone zone;
private boolean lenient = true;
private Locale locale;
private int firstDayOfWeek, minimalDaysInFirstWeek;
public Builder() {}
public Builder setInstant(long instant) {
if (fields != null) {
throw new IllegalStateException();
}
this.instant = instant;
nextStamp = COMPUTED;
return this;
}
//...省略n多set()方法
public Calendar build() {
if (locale == null) {
locale = Locale.getDefault();
}
if (zone == null) {
zone = TimeZone.getDefault();
}
Calendar cal;
if (type == null) {
type = locale.getUnicodeLocaleType(&quot;ca&quot;);
}
if (type == null) {
if (locale.getCountry() == &quot;TH&quot; &amp;&amp; locale.getLanguage() == &quot;th&quot;) {
type = &quot;buddhist&quot;;
} else {
type = &quot;gregory&quot;;
}
}
switch (type) {
case &quot;gregory&quot;:
cal = new GregorianCalendar(zone, locale, true);
break;
case &quot;iso8601&quot;:
GregorianCalendar gcal = new GregorianCalendar(zone, locale, true);
// make gcal a proleptic Gregorian
gcal.setGregorianChange(new Date(Long.MIN_VALUE));
// and week definition to be compatible with ISO 8601
setWeekDefinition(MONDAY, 4);
cal = gcal;
break;
case &quot;buddhist&quot;:
cal = new BuddhistCalendar(zone, locale);
cal.clear();
break;
case &quot;japanese&quot;:
cal = new JapaneseImperialCalendar(zone, locale, true);
break;
default:
throw new IllegalArgumentException(&quot;unknown calendar type: &quot; + type);
}
cal.setLenient(lenient);
if (firstDayOfWeek != 0) {
cal.setFirstDayOfWeek(firstDayOfWeek);
cal.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);
}
if (isInstantSet()) {
cal.setTimeInMillis(instant);
cal.complete();
return cal;
}
if (fields != null) {
boolean weekDate = isSet(WEEK_YEAR) &amp;&amp; fields[WEEK_YEAR] &gt; fields[YEAR];
if (weekDate &amp;&amp; !cal.isWeekDateSupported()) {
throw new IllegalArgumentException(&quot;week date is unsupported by &quot; + type);
}
for (int stamp = MINIMUM_USER_STAMP; stamp &lt; nextStamp; stamp++) {
for (int index = 0; index &lt;= maxFieldIndex; index++) {
if (fields[index] == stamp) {
cal.set(index, fields[NFIELDS + index]);
break;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? fields[NFIELDS + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ? fields[NFIELDS + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
cal.setWeekDate(fields[NFIELDS + WEEK_YEAR], weekOfYear, dayOfWeek);
}
cal.complete();
}
return cal;
}
}
}
```
看了上面的代码我有一个问题请你思考一下既然已经有了getInstance()工厂方法来创建Calendar类对象为什么还要用Builder来创建Calendar类对象呢这两者之间的区别在哪里呢
实际上,在前面讲到这两种模式的时候,我们对它们之间的区别做了详细的对比,现在,我们再来一块回顾一下。工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。
网上有一个经典的例子很好地解释了两者的区别。
>
顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作不同的披萨。
粗看Calendar的Builder类的build()方法你可能会觉得它有点像工厂模式。你的感觉没错前面一半代码确实跟getInstance()工厂方法类似根据不同的type创建了不同的Calendar子类。实际上后面一半代码才属于标准的建造者模式根据setXXX()方法设置的参数来定制化刚刚创建的Calendar子类对象。
你可能会说,这还能算是建造者模式吗?我用[第46讲](https://time.geekbang.org/column/article/199674)的一段话来回答你:
>
我们也不要太学院派,非得把工厂模式、建造者模式分得那么清楚,我们需要知道的是,每个模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,我们才能不生搬硬套,才能灵活应用,甚至可以混用各种模式,创造出新的模式来解决特定场景的问题。
实际上从Calendar这个例子我们也能学到不要过于死板地套用各种模式的原理和实现不要不敢做丝毫的改动。模式是死的用的人是活的。在实际上的项目开发中不仅各种模式可以混合在一起使用而且具体的代码实现也可以根据具体的功能需求做灵活的调整。
## 装饰器模式在Collections类中的应用
我们前面讲到Java IO类库是装饰器模式的非常经典的应用。实际上Java的Collections类也用到了装饰器模式。
Collections类是一个集合容器的工具类提供了很多静态方法用来创建各种集合容器比如通过unmodifiableColletion()静态方法来创建UnmodifiableCollection类对象。而这些容器类中的UnmodifiableCollection类、CheckedCollection和SynchronizedCollection类就是针对Collection类的装饰器类。
因为刚刚提到的这三个装饰器类在代码结构上几乎一样所以我们这里只拿其中的UnmodifiableCollection类来举例讲解一下。UnmodifiableCollection类是Collections类的一个内部类相关代码我摘抄到了下面你可以先看下。
```
public class Collections {
private Collections() {}
public static &lt;T&gt; Collection&lt;T&gt; unmodifiableCollection(Collection&lt;? extends T&gt; c) {
return new UnmodifiableCollection&lt;&gt;(c);
}
static class UnmodifiableCollection&lt;E&gt; implements Collection&lt;E&gt;, Serializable {
private static final long serialVersionUID = 1820017752578914078L;
final Collection&lt;? extends E&gt; c;
UnmodifiableCollection(Collection&lt;? extends E&gt; c) {
if (c==null)
throw new NullPointerException();
this.c = c;
}
public int size() {return c.size();}
public boolean isEmpty() {return c.isEmpty();}
public boolean contains(Object o) {return c.contains(o);}
public Object[] toArray() {return c.toArray();}
public &lt;T&gt; T[] toArray(T[] a) {return c.toArray(a);}
public String toString() {return c.toString();}
public Iterator&lt;E&gt; iterator() {
return new Iterator&lt;E&gt;() {
private final Iterator&lt;? extends E&gt; i = c.iterator();
public boolean hasNext() {return i.hasNext();}
public E next() {return i.next();}
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer&lt;? super E&gt; action) {
// Use backing collection version
i.forEachRemaining(action);
}
};
}
public boolean add(E e) {
throw new UnsupportedOperationException();
}
public boolean remove(Object o) {
hrow new UnsupportedOperationException();
}
public boolean containsAll(Collection&lt;?&gt; coll) {
return c.containsAll(coll);
}
public boolean addAll(Collection&lt;? extends E&gt; coll) {
throw new UnsupportedOperationException();
}
public boolean removeAll(Collection&lt;?&gt; coll) {
throw new UnsupportedOperationException();
}
public boolean retainAll(Collection&lt;?&gt; coll) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}
// Override default methods in Collection
@Override
public void forEach(Consumer&lt;? super E&gt; action) {
c.forEach(action);
}
@Override
public boolean removeIf(Predicate&lt;? super E&gt; filter) {
throw new UnsupportedOperationException();
}
@SuppressWarnings(&quot;unchecked&quot;)
@Override
public Spliterator&lt;E&gt; spliterator() {
return (Spliterator&lt;E&gt;)c.spliterator();
}
@SuppressWarnings(&quot;unchecked&quot;)
@Override
public Stream&lt;E&gt; stream() {
return (Stream&lt;E&gt;)c.stream();
}
@SuppressWarnings(&quot;unchecked&quot;)
@Override
public Stream&lt;E&gt; parallelStream() {
return (Stream&lt;E&gt;)c.parallelStream();
}
}
}
```
看了上面的代码请你思考一下为什么说UnmodifiableCollection类是Collection类的装饰器类呢这两者之间可以看作简单的接口实现关系或者类继承关系吗
我们前面讲过装饰器模式中的装饰器类是对原始类功能的增强。尽管UnmodifiableCollection类可以算是对Collection类的一种功能增强但这点还不具备足够的说服力来断定UnmodifiableCollection就是Collection类的装饰器类。
实际上最关键的一点是UnmodifiableCollection的构造函数接收一个Collection类对象然后对其所有的函数进行了包裹Wrap重新实现比如add()函数或者简单封装比如stream()函数。而简单的接口实现或者继承并不会如此来实现UnmodifiableCollection类。所以从代码实现的角度来说UnmodifiableCollection类是典型的装饰器类。
## 适配器模式在Collections类中的应用
在[第51讲](https://time.geekbang.org/column/article/205912)中我们讲到适配器模式可以用来兼容老的版本接口。当时我们举了一个JDK的例子这里我们再重新仔细看一下。
老版本的JDK提供了Enumeration类来遍历容器。新版本的JDK用Iterator类替代Enumeration类来遍历容器。为了兼容老的客户端代码使用老版本JDK的代码我们保留了Enumeration类并且在Collections类中仍然保留了enumaration()静态方法因为我们一般都是通过这个静态函数来创建一个容器的Enumeration类对象
不过保留Enumeration类和enumeration()函数,都只是为了兼容,实际上,跟适配器没有一点关系。那到底哪一部分才是适配器呢?
在新版本的JDK中Enumeration类是适配器类。它适配的是客户端代码使用Enumeration类和新版本JDK中新的迭代器Iterator类。不过从代码实现的角度来说这个适配器模式的代码实现跟经典的适配器模式的代码实现差别稍微有点大。enumeration()静态函数的逻辑和Enumeration适配器类的代码耦合在一起enumeration()静态函数直接通过new的方式创建了匿名类对象。具体的代码如下所示
```
/**
* Returns an enumeration over the specified collection. This provides
* interoperability with legacy APIs that require an enumeration
* as input.
*
* @param &lt;T&gt; the class of the objects in the collection
* @param c the collection for which an enumeration is to be returned.
* @return an enumeration over the specified collection.
* @see Enumeration
*/
public static &lt;T&gt; Enumeration&lt;T&gt; enumeration(final Collection&lt;T&gt; c) {
return new Enumeration&lt;T&gt;() {
private final Iterator&lt;T&gt; i = c.iterator();
public boolean hasMoreElements() {
return i.hasNext();
}
public T nextElement() {
return i.next();
}
};
}
```
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天我重点讲了工厂模式、建造者模式、装饰器模式、适配器模式这四种模式在Java JDK中的应用主要目的是给你展示真实项目中是如何灵活应用设计模式的。
从今天的讲解中,我们可以学习到,尽管在之前的理论讲解中,我们都有讲到每个模式的经典代码实现,但是,在真实的项目开发中,这些模式的应用更加灵活,代码实现更加自由,可以根据具体的业务场景、功能需求,对代码实现做很大的调整,甚至还可能会对模式本身的设计思路做调整。
比如Java JDK中的Calendar类就耦合了业务功能代码、工厂方法、建造者类三种类型的代码而且在建造者类的build()方法中,前半部分是工厂方法的代码实现,后半部分才是真正的建造者模式的代码实现。这也告诉我们,在项目中应用设计模式,切不可生搬硬套,过于学院派,要学会结合实际情况做灵活调整,做到心中无剑胜有剑。
## 课堂讨论
在Java中经常用到的StringBuilder类是否是建造者模式的应用呢你可以试着像我一样从源码的角度去剖析一下。
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,232 @@
<audio id="audio" title="77 | 开源实战一通过剖析Java JDK源码学习灵活应用设计模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/90/14/90b46cedf919ee8579a87a47af58eb14.mp3"></audio>
上一节课我们讲解了工厂模式、建造者模式、装饰器模式、适配器模式在Java JDK中的应用其中Calendar类用到了工厂模式和建造者模式Collections类用到了装饰器模式、适配器模式。学习的重点是让你了解在真实的项目中模式的实现和应用更加灵活、多变会根据具体的场景做实现或者设计上的调整。
今天我们继续延续这个话题再重点讲一下模板模式、观察者模式这两个模式在JDK中的应用。除此之外我还会对在理论部分已经讲过的一些模式在JDK中的应用做一个汇总带你一块回忆复习一下。
话不多说,让我们正式开始今天的学习吧!
## 模板模式在Collections类中的应用
我们前面提到策略、模板、职责链三个模式常用在框架的设计中提供框架的扩展点让框架使用者在不修改框架源码的情况下基于扩展点定制化框架的功能。Java中的Collections类的sort()函数就是利用了模板模式的这个扩展特性。
首先我们看下Collections.sort()函数是如何使用的。我写了一个示例代码如下所示。这个代码实现了按照不同的排序方式按照年龄从小到大、按照名字字母序从小到大、按照成绩从大到小对students数组进行排序。
```
public class Demo {
public static void main(String[] args) {
List&lt;Student&gt; students = new ArrayList&lt;&gt;();
students.add(new Student(&quot;Alice&quot;, 19, 89.0f));
students.add(new Student(&quot;Peter&quot;, 20, 78.0f));
students.add(new Student(&quot;Leo&quot;, 18, 99.0f));
Collections.sort(students, new AgeAscComparator());
print(students);
Collections.sort(students, new NameAscComparator());
print(students);
Collections.sort(students, new ScoreDescComparator());
print(students);
}
public static void print(List&lt;Student&gt; students) {
for (Student s : students) {
System.out.println(s.getName() + &quot; &quot; + s.getAge() + &quot; &quot; + s.getScore());
}
}
public static class AgeAscComparator implements Comparator&lt;Student&gt; {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
}
public static class NameAscComparator implements Comparator&lt;Student&gt; {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
}
public static class ScoreDescComparator implements Comparator&lt;Student&gt; {
@Override
public int compare(Student o1, Student o2) {
if (Math.abs(o1.getScore() - o2.getScore()) &lt; 0.001) {
return 0;
} else if (o1.getScore() &lt; o2.getScore()) {
return 1;
} else {
return -1;
}
}
}
}
```
结合刚刚这个例子我们再来看下为什么说Collections.sort()函数用到了模板模式?
Collections.sort()实现了对集合的排序。为了扩展性它将其中“比较大小”这部分逻辑委派给用户来实现。如果我们把比较大小这部分逻辑看作整个排序逻辑的其中一个步骤那我们就可以把它看作模板模式。不过从代码实现的角度来看它看起来有点类似之前讲过的JdbcTemplate并不是模板模式的经典代码实现而是基于Callback回调机制来实现的。
不过在其他资料中我还看到有人说Collections.sort()使用的是策略模式。这样的说法也不是没有道理的。如果我们并不把“比较大小”看作排序逻辑中的一个步骤,而是看作一种算法或者策略,那我们就可以把它看作一种策略模式的应用。
不过这也不是典型的策略模式我们前面讲到在典型的策略模式中策略模式分为策略的定义、创建、使用这三部分。策略通过工厂模式来创建并且在程序运行期间根据配置、用户输入、计算结果等这些不确定因素动态决定使用哪种策略。而在Collections.sort()函数中,策略的创建并非通过工厂模式,策略的使用也非动态确定。
## 观察者模式在JDK中的应用
在讲到观察者模式的时候我们重点讲解了Google Guava的EventBus框架它提供了观察者模式的骨架代码。使用EventBus我们不需要从零开始开发观察者模式。实际上Java JDK也提供了观察者模式的简单框架实现。在平时的开发中如果我们不希望引入Google Guava开发库可以直接使用Java语言本身提供的这个框架类。
不过它比EventBus要简单多了只包含两个类java.util.Observable和java.util.Observer。前者是被观察者后者是观察者。它们的代码实现也非常简单为了方便你查看我直接copy-paste到了这里。
```
public interface Observer {
void update(Observable o, Object arg);
}
public class Observable {
private boolean changed = false;
private Vector&lt;Observer&gt; obs;
public Observable() {
obs = new Vector&lt;&gt;();
}
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
public void notifyObservers() {
notifyObservers(null);
}
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i&gt;=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
public synchronized void deleteObservers() {
obs.removeAllElements();
}
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
}
```
对于Observable、Observer的代码实现大部分都很好理解我们重点来看其中的两个地方。一个是changed成员变量另一个是notifyObservers()函数。
**我们先来看changed成员变量。**
它用来表明被观察者Observable有没有状态更新。当有状态更新时我们需要手动调用setChanged()函数将changed变量设置为true这样才能在调用notifyObservers()函数的时候真正触发观察者Observer执行update()函数。否则即便你调用了notifyObservers()函数观察者的update()函数也不会被执行。
也就是说当通知观察者被观察者状态更新的时候我们需要依次调用setChanged()和notifyObservers()两个函数单独调用notifyObservers()函数是不起作用的。你觉得这样的设计是不是多此一举呢?这个问题留给你思考,你可以在留言区说说你的看法。
**我们再来看notifyObservers()函数。**
为了保证在多线程环境下添加、移除、通知观察者三个操作之间不发生冲突Observable类中的大部分函数都通过synchronized加了锁不过也有特例notifyObservers()这函数就没有加synchronized锁。这是为什么呢在JDK的代码实现中notifyObservers()函数是如何保证跟其他函数操作不冲突的呢?这种加锁方法是否存在问题?又存在什么问题呢?
notifyObservers()函数之所以没有像其他函数那样,一把大锁加在整个函数上,主要还是出于性能的考虑。<br>
notifyObservers()函数依次执行每个观察者的update()函数每个update()函数执行的逻辑提前未知有可能会很耗时。如果在notifyObservers()函数上加synchronized锁notifyObservers()函数持有锁的时间就有可能会很长这就会导致其他线程迟迟获取不到锁影响整个Observable类的并发性能。
我们知道Vector类不是线程安全的在多线程环境下同时添加、删除、遍历Vector类对象中的元素会出现不可预期的结果。所以在JDK的代码实现中为了避免直接给notifyObservers()函数加锁而出现性能问题JDK采用了一种折中的方案。这个方案有点类似于我们之前讲过的让迭代器支持”快照“的解决方案。
在notifyObservers()函数中我们先拷贝一份观察者列表赋值给函数的局部变量我们知道局部变量是线程私有的并不在线程间共享。这个拷贝出来的线程私有的观察者列表就相当于一个快照。我们遍历快照逐一执行每个观察者的update()函数。而这个遍历执行的过程是在快照这个局部变量上操作的,不存在线程安全问题,不需要加锁。所以,我们只需要对拷贝创建快照的过程加锁,加锁的范围减少了很多,并发性能提高了。
为什么说这是一种折中的方案呢?这是因为,这种加锁方法实际上是存在一些问题的。在创建好快照之后,添加、删除观察者都不会更新快照,新加入的观察者就不会被通知到,新删除的观察者仍然会被通知到。这种权衡是否能接受完全看你的业务场景。实际上,这种处理方式也是多线程编程中减小锁粒度、提高并发性能的常用方法。
## 单例模式在Runtime类中的应用
JDK中java.lang.Runtime类就是一个单例类。这个类你有没有比较眼熟呢是的我们之前讲到Callback回调的时候添加shutdown hook就是通过这个类来实现的。
每个Java应用在运行时会启动一个JVM进程每个JVM进程都只对应一个Runtime实例用于查看JVM状态以及控制JVM行为。进程内唯一所以比较适合设计为单例。在编程的时候我们不能自己去实例化一个Runtime对象只能通过getRuntime()静态方法来获得。
Runtime类的的代码实现如下所示。这里面只包含部分相关代码其他代码做了省略。从代码中我们也可以看出它使用了最简单的饿汉式的单例实现方式。
```
/**
* Every Java application has a single instance of class
* &lt;code&gt;Runtime&lt;/code&gt; that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the &lt;code&gt;getRuntime&lt;/code&gt; method.
* &lt;p&gt;
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
//....
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission(&quot;shutdownHooks&quot;));
}
ApplicationShutdownHooks.add(hook);
}
//...
}
```
## 其他模式在JDK中的应用汇总
实际上我们在讲解理论部分的时候已经讲过很多模式在Java JDK中的应用了。这里我们一块再回顾一下如果你对哪一部分有所遗忘可以再回过头去看下。
在讲到模板模式的时候我们结合Java Servlet、JUnit TestCase、Java InputStream、Java AbstractList四个例子来具体讲解了它的两个作用扩展性和复用性。<br>
在讲到享元模式的时候我们讲到Integer类中的-128~127之间的整型对象是可以复用的还讲到String类型中的常量字符串也是可以复用的。这些都是享元模式的经典应用。
在讲到职责链模式的时候,我们讲到Java Servlet中的Filter就是通过职责链来实现的同时还对比了Spring中的interceptor。实际上拦截器、过滤器这些功能绝大部分都是采用职责链模式来实现的。
在讲到的迭代器模式的时候我们重点剖析了Java中Iterator迭代器的实现手把手带你实现了一个针对线性数据结构的迭代器。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
这两节课主要剖析了JDK中用到的几个经典设计模式其中重点剖析的有工厂模式、建造者模式、装饰器模式、适配器模式、模板模式、观察者模式除此之外我们还汇总了其他模式在JDK中的应用比如单例模式、享元模式、职责链模式、迭代器模式。
实际上,源码都很简单,理解起来都不难,都没有跳出我们之前讲解的理论知识的范畴。学习的重点并不是表面上去理解、记忆某某类用了某某设计模式,而是让你了解我反复强调的一点,也是标题中突出的一点,在真实的项目开发中,如何灵活应用设计模式,做到活学活用,能够根据具体的场景、需求,做灵活的设计和实现上的调整。这也是模式新手和老手的最大区别。
## 课堂讨论
针对Java JDK中观察者模式的代码实现我有两个问题请你思考。
1. 每个函数都加一把synchronized大锁会不会影响并发性能有没有优化的方法
1. changed成员变量是否多此一举
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,95 @@
<audio id="audio" title="78 | 开源实战二从Unix开源开发学习应对大型复杂项目开发" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d5/08/d5ec78044cc2a9bf12ed7b3eaaf21f08.mp3"></audio>
软件开发的难度无外乎两点,一是技术难,意思是说,代码量不一定多,但要解决的问题比较难,需要用到一些比较深的技术解决方案或者算法,不是靠“堆人”就能搞定的,比如自动驾驶、图像识别、高性能消息队列等;二是复杂度,意思是说,技术不难,但项目很庞大,业务复杂,代码量多,参与开发的人多,比如物流系统、财务系统等。第一点涉及细分专业的领域知识,跟我们专栏要讲的设计、编码无关,所以我们重点来讲第二点,如何应对软件开发的复杂度。
简单的“hello world”程序谁都能写得出来。几千行的代码谁都能维护得了。但是当代码超过几万行、十几万甚至几十万行、上百万行的时候软件的复杂度就会呈指数级增长。这种情况下我们不仅仅要求程序运行得了运行得正确还要求代码看得懂、维护得了。实际上复杂度不仅仅体现在代码本身还体现在协作研发上如何管理庞大的团队来进行有条不紊地协作开发也是一个很复杂的难题。
如何应对复杂软件开发Unix开源项目就是一个值得学习的例子。
Unix从1969年诞生一直演进至今代码量有几百万行如此庞大的项目开发能够如此完美的协作开发并且长期维护保持足够的代码质量这里面有很多成功的经验可以借鉴。所以接下来我们就以Unix开源项目的开发为引子分三节课的时间通过下面三个话题详细地讲讲应对复杂软件开发的方法论。希望这些经验能为你所用在今后面对复杂项目开发的时候能让你有条不紊、有章可循地从容应对。
- 从设计原则和思想的角度来看,如何应对庞大而复杂的项目开发?
- 从研发管理和开发技巧的角度来看,如何应对庞大而复杂的项目开发?
- 聚焦在Code Review上来看如何通过Code Reviwe保持项目的代码质量
话不多说,让我们正式开始今天的学习吧!
## 封装与抽象
在Unix、Linux系统中有一句经典的话“Everything is a file”翻译成中文就是“一切皆文件”。这句话的意思就是在Unix、Linux系统中很多东西都被抽象成“文件”这样一个概念比如Socket、驱动、硬盘、系统信息等。它们使用文件系统的路径作为统一的命名空间namespace使用统一的read、write标准函数来访问。
比如我们要查看CPU的信息在Linux系统中我们只需要使用Vim、Gedit等编辑器或者cat命令像打开其他文件一样打开/proc/cpuinfo就能查看到相应的信息。除此之外我们还可以通过查看/proc/uptime文件了解系统运行了多久查看/proc/version了解系统的内核版本等。
实际上,“一切皆文件”就体现了封装和抽象的设计思想。
封装了不同类型设备的访问细节,抽象为统一的文件访问方式,更高层的代码就能基于统一的访问方式,来访问底层不同类型的设备。这样做的好处是,隔离底层设备访问的复杂性。统一的访问方式能够简化上层代码的编写,并且代码更容易复用。
除此之外,抽象和封装还能有效控制代码复杂性的蔓延,将复杂性封装在局部代码中,隔离实现的易变性,提供简单、统一的访问接口,让其他模块来使用,其他模块基于抽象的接口而非具体的实现编程,代码会更加稳定。
## 分层与模块化
前面我们也提到,模块化是构建复杂系统的常用手段。
对于像Unix这样的复杂系统没有人能掌控所有的细节。之所以我们能开发出如此复杂的系统并且能维护得了最主要的原因就是将系统划分成各个独立的模块比如进程调度、进程通信、内存管理、虚拟文件系统、网络接口等模块。不同的模块之间通过接口来进行通信模块之间耦合很小每个小的团队聚焦于一个独立的高内聚模块来开发最终像搭积木一样将各个模块组装起来构建成一个超级复杂的系统。
除此之外Unix、Linux等大型系统之所以能做到几百、上千人有条不紊地协作开发也归功于模块化做得好。不同的团队负责不同的模块开发这样即便在不了解全部细节的情况下管理者也能协调各个模块让整个系统有效运转。
实际上,除了模块化之外,分层也是我们常用来架构复杂系统的方法。
我们常说计算机领域的任何问题都可以通过增加一个间接的中间层来解决这本身就体现了分层的重要性。比如Unix系统也是基于分层开发的它可以大致上分为三层分别是内核、系统调用、应用层。每一层都对上层封装实现细节暴露抽象的接口来调用。而且任意一层都可以被重新实现不会影响到其他层的代码。
面对复杂系统的开发,我们要善于应用分层技术,把容易复用、跟具体业务关系不大的代码,尽量下沉到下层,把容易变动、跟具体业务强相关的代码,尽量上移到上层。
## 基于接口通信
刚刚我们讲了分层、模块化那不同的层之间、不同的模块之间是如何通信的呢一般来讲都是通过接口调用。在设计模块module或者层layer要暴露的接口的时候我们要学会隐藏实现接口从命名到定义都要抽象一些尽量少涉及具体的实现细节。
比如Unix系统提供的open()文件操作函数底层实现非常复杂涉及权限控制、并发控制、物理存储但我们用起来却非常简单。除此之外因为open()函数基于抽象而非具体的实现来定义所以我们在改动open()函数的底层实现的时候,并不需要改动依赖它的上层代码。
## 高内聚、松耦合
高内聚、松耦合是一个比较通用的设计思想内聚性好、耦合少的代码能让我们在修改或者阅读代码的时候聚集到在一个小范围的模块或者类中不需要了解太多其他模块或类的代码让我们的焦点不至于太发散也就降低了阅读和修改代码的难度。而且因为依赖关系简单耦合小修改代码不会牵一发而动全身代码改动比较集中引入bug的风险也就减少了很多。
实际上,刚刚讲到的很多方法,比如封装、抽象、分层、模块化、基于接口通信,都能有效地实现代码的高内聚、松耦合。反过来,代码的高内聚、松耦合,也就意味着,抽象、封装做到比较到位、代码结构清晰、分层和模块化合理、依赖关系简单,那代码整体的质量就不会太差。即便某个具体的类或者模块设计得不怎么合理,代码质量不怎么高,影响的范围也是非常有限的。我们可以聚焦于这个模块或者类做相应的小型重构。而相对于代码结构的调整,这种改动范围比较集中的小型重构的难度就小多了。
## 为扩展而设计
越是复杂项目,越要在前期设计上多花点时间。提前思考项目中未来可能会有哪些功能需要扩展,提前预留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构的情况下,轻松地添加新功能。
做到代码可扩展需要代码满足开闭原则。特别是像Unix这样的开源项目有n多人参与开发任何人都可以提交代码到代码库中。代码满足开闭原则基于扩展而非修改来添加新功能最小化、集中化代码改动避免新代码影响到老代码降低引入bug的风险。
除了满足开闭原则,做到代码可扩展,我们前面也提到很多方法,比如封装和抽象,基于接口编程等。识别出代码可变部分和不可变部分,将可变部分封装起来,隔离变化,提供抽象化的不可变接口,供上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。
## KISS首要原则
简单清晰、可读性好是任何大型软件开发要遵循的首要原则。只要可读性好即便扩展性不好顶多就是多花点时间、多改动几行代码的事情。但是如果可读性不好连看都看不懂那就不是多花时间可以解决得了的了。如果你对现有代码的逻辑似懂非懂抱着尝试的心态去修改代码引入bug的可能性就会很大。
不管是自己还是团队在参与大型项目开发的时候要尽量避免过度设计、过早优化在扩展性和可读性有冲突的时候或者在两者之间权衡模棱两可的时候应该选择遵循KISS原则首选可读性。
## 最小惊奇原则
《Unix编程艺术》一书中提到一个Unix的经典设计原则叫“最小惊奇原则”英文是“The Least Surprise Principle”。实际上这个原则等同于“遵守开发规范”意思是在做设计或者编码的时候要遵守统一的开发规范避免反直觉的设计。实际上关于这一点我们在前面的编码规范部分也讲到过。
遵从统一的编码规范,所有的代码都像一个人写出来的,能有效地减少阅读干扰。在大型软件开发中,参与开发的人员很多,如果每个人都按照自己的编码习惯来写代码,那整个项目的代码风格就会千奇百怪,这个类是这种编码风格,另一个类又是另外一种风格。在阅读的时候,我们要不停地切换去适应不同的编码风格,可读性就变差了。所以,对于大型项目的开发来说,我们要特别重视遵守统一的开发规范。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天我们主要从设计原则和思想的角度也可以说是从设计开发的角度来学习如何应对复杂软件开发。我总计了7点我认为比较重要的。这7点前面我们都详细讲过如果你对哪块理解得不够清楚可以回过头去再看下。这7点分别是
- 封装与抽象
- 分层与模块化
- 基于接口通信
- 高内聚、松耦合
- 为扩展而设计
- KISS首要原则
- 最小惊奇原则
当然这7点之间并不是相互独立的有几点是互相支持的比如“高内聚、松耦合”与抽象封装、分层模块化、基于接口通信。有几点是互相冲突的 比如KISS原则与为扩展而设计这都需要我们根据实际情况去权衡。
## 课堂讨论
从设计原则和思想的角度来看,你觉得哪些原则或思想在大型软件开发中最能发挥作用,最能有效地应对代码的复杂性?
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,65 @@
<audio id="audio" title="79 | 开源实战二从Unix开源开发学习应对大型复杂项目开发" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cd/f1/cd1985cbc9ec70c219447bd8086abaf1.mp3"></audio>
我们知道项目越复杂、代码量越多、参与开发人员越多、开发维护时间越长我们就越是要重视代码质量。代码质量下降会导致项目研发困难重重比如开发效率低招了很多人天天加班出活却不多线上bug频发查找bug困难领导发飙中层束手无策工程师抱怨不断。
导致代码质量不高的原因有很多比如代码无注释无文档命名差层次结构不清晰调用关系混乱到处hardcode充斥着各种临时解决方案等等。那怎么才能时刻保证代码质量呢当然首要的是团队技术素质要过硬能够适当地利用设计原则、思想、模式编写高质量的代码。除此之外还有一些外在的方法可循。
今天,我就从研发管理和开发技巧的角度来带你看下,面对大型复杂项目的开发,如何长期保证代码质量,让代码长期可维护。
话不多说,让我们正式开始今天的学习吧!
## 1. 吹毛求疵般地执行编码规范
严格执行代码规范可以使一个项目乃至整个公司的代码具有完全统一的风格就像同一个人编写的。而且命名良好的变量、函数、类和注释也可以提高代码的可读性。编码规范不难掌握关键是要严格执行。在Code Review时我们一定要严格要求看到不符合规范的代码一定要指出并要求修改。
但是据我了解实际情况往往事与愿违。虽然大家都知道优秀的代码规范是怎样的但在具体写代码的过程中执行得却不好。我觉得这种情况产生的主要原因还是不够重视。很多人会觉得一个变量或者函数命名成啥样关系并不大。所以命名时不推敲注释也不写Code Review的时候也都一副事不关己的心态觉得没必要太抠细节。日积月累项目代码就会变得越来越差。所以我这里还是要强调一下细节决定成败代码规范的严格执行极为关键。
## 2.编写高质量的单元测试
单元测试是最容易执行且对提高代码质量见效最快的方法之一。高质量的单元测试不仅仅要求测试覆盖率要高,还要求测试的全面性,除了测试正常逻辑的执行之外,还要重点、全面地测试异常下的执行情况。毕竟代码出问题的地方大部分都发生在异常、边界条件下。
对于大型复杂项目集成测试、黑盒测试都很难测试全面因为组合爆炸穷举所有测试用例的成本很高几乎是不可能的。单元测试就是很好的补充。它可以在类、函数这些细粒度的代码层面保证代码运行无误。底层细粒度的代码bug少了组合起来构建而成的整个系统的bug也就相应的减少了。
## 3.不流于形式的Code Review
如果说很多工程师对单元测试不怎么重视那对Code Review就是不怎么接受。我之前跟一些同行聊起Code Review的时候很多人的反应是这玩意儿不可能很好地执行形式大于效果纯粹是浪费时间。是的即便Code Review做得再流畅也是要花时间的。所以在业务开发任务繁重的时候Code Review往往会流于形式、虎头蛇尾效果确实不怎么好。
但我们并不能因此就否定Code Review本身的价值。在Google、Facebook等外企中Code Review应用得非常成功已经成为了开发流程中不可或缺的一部分。所以要想真正发挥Code Review的作用关键还是要执行到位不能流于形式。
## 4.开发未动、文档先行
对大部分工程师来说编写技术文档是件挺让人“反感”的事情。一般来讲在开发某个系统或者重要模块或者功能之前我们应该先写技术文档然后发送给同组或者相关同事审查在审查没有问题的情况下再开发。这样能够保证事先达成共识开发出来的东西不至于走样。而且当开发完成之后进行Code Review的时候代码审查者通过阅读开发文档也可以快速理解代码。
除此之外,对于团队和公司来讲,文档是重要的财富。对新人熟悉代码或任务的交接等,技术文档很有帮助。而且,作为一个规范化的技术团队,技术文档是一种摒弃作坊式开发和个人英雄主义的有效方法,是保证团队有效协作的途径。
## 5.持续重构、重构、重构
我个人比较反对平时不注重代码质量,堆砌烂代码,实在维护不了了就大刀阔斧地重构甚至重写。有的时候,因为项目代码太多,重构很难做到彻底,最后又搞出来一个四不像的怪物,这就更麻烦了!
优秀的代码或架构不是一开始就能设计好的就像优秀的公司或产品也都是迭代出来的。我们无法100%预见未来的需求,也没有足够的精力、时间、资源为遥远的未来买单。所以,随着系统的演进,重构是不可避免的。
虽然我刚刚说不支持大刀阔斧、推倒重来式的大重构,但持续的小重构我还是比较提倡的。它也是时刻保证代码质量、防止代码腐化的有效手段。换句话说,不要等到问题堆得太多了再去解决,要时刻有人对代码整体质量负责任,平时没事就改改代码。千万不要觉得重构代码就是浪费时间,不务正业!
特别是一些业务开发团队有时候为了快速完成一个业务需求只追求速度到处hard code在完全不考虑非功能性需求、代码质量的情况下堆砌烂代码。实际上这种情况还是比较常见的。不过没关系等你有时间了一定要记着重构不然烂代码越堆越多总有一天代码会变得无法维护。
## 6.对项目与团队进行拆分
我们知道团队人比较少比如十几个人的时候代码量不多不超过10万行怎么开发、怎么管理都没问题大家互相都比较了解彼此做的东西。即便代码质量太差了我们大不了把它重写一遍。但是对于一个大型项目来说参与开发的人员会比较多代码量很大有几十万、甚至几百万行代码有几十、甚至几百号人同时开发维护那研发管理就变得极其重要。
面对大型复杂项目,我们不仅仅需要对代码进行拆分,还需要对研发团队进行拆分。上一节课我们讲到了一些代码拆分的方法,比如模块化、分层等。同理,我们也可以把大团队拆成几个小团队。每个小团队对应负责一个小的项目(模块、微服务等),这样每个团队负责的项目包含的代码都不至于很多,也不至于出现代码质量太差无法维护的情况。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
实际上我刚刚讲的6条方法论应该都没啥新奇的也没有葵花宝典似的杀手锏说出来感觉都很简单。而且现在互联网这么发达信息都很透明所以大方向我觉得你应该都知道具体的策略和架构各家也都差不多最后谁做得好关键在于执行和细节。
我经常听人说我们做了单元测试、Code Review啊但到最后项目还是一堆bug代码质量还是很差。这个时候我们就要去思考一下单元测试、Code Review做得到底够不够好从决策到执行再到考核是否形成了闭环不要口号喊的100分落实到执行只有50分最后又没有很好的考核机制好坏大家也都不知道。所以一句话总结一下切忌敏于言而讷于行。
除此之外,我们刚刚讲的所有方法都治标不治本。软件开发过程中的问题往往千奇百怪。要想每个问题都能顺利解决,除了理论知识和经验之外,更重要的是要具备分析问题、解决问题的能力。这也是为什么很多公司很重视应届生招聘,希望从一开始就招聘一些有潜力的员工。找到对的人、用对好的人,打造优秀的技术文化,才是一直保持卓越的根本。
## 课堂讨论
从研发管理和开发技巧的角度,你还有哪些能够有效保持项目代码质量的经验,可以分享给大家?
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,99 @@
<audio id="audio" title="80 | 开源实战二从Unix开源开发学习应对大型复杂项目开发" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/aa/03/aadd7bc2c8b42b5390ecb5820fbd6503.mp3"></audio>
上两节课我们分别从代码编写、研发管理的角度学习了如何应对大型复杂软件开发。在研发管理这一部分我们又讲到比较重要的几点它们分别是编码规范、单元测试、持续重构和Code Review。其中前三点在专栏的理论部分都有比较详细的讲解而唯独Code Review我们还没有讲过所以今天我就借机会和你补充一下这一部分的内容。
很多年前我跟一个有十几年研发经验的某一线大厂的技术专家聊天聊天中我提起了Code Review他便对Code Review一顿否定。他说Code Review比较浪费时间往往会虎头蛇尾不可能在企业中很好地落地执行。当我又提起Code Review在Google执行得很好并且是已经习以为常的开发流程的时候他竟然说这绝对不可能。
一个技术不错可以玩转各种架构、框架、中间件的资深IT从业者居然对Code Review有如此的偏见这到底是哪里出了问题呢我觉得问题主要还是出自认知上。
所以今天我并不打算讲关于如何做Code Review的方法论我更希望充当一个Code Review布道师的角色讲一讲为什么要进行Code ReviewCode Review的价值在哪里让你重视、认可Code Review。因为我觉得只要从认知上接受了Code Review对于高智商的IT人群来说搞清楚如何做Code Review并不是件难事。而且Google也开源了它自己的Code Review最佳实践网上很容易搜到你完全可以对照着来做。
话不多说,让我们正式开始今天的内容吧!
## 我为什么如此强调Code Review
Code Review中文叫代码审查。据我了解在国内绝大部分的互联网企业里面很少有将Code Review执行得很好的这其中包括BAT这些大厂。特别是在一些需求变动大、项目工期紧的业务开发部门就更不可能有Code Review流程了。代码写完之后随手就提交上去然后丢给测试狠命测发现bug再修改。
相反一些外企非常重视Code Review特别是FLAG这些大厂Code Review落地执行得非常好。在Google工作的几年里我也切实体会到了Code Review的好处。这里我就结合我自身的真实感受讲一讲Code Review的价值试着“说服”一下你。
### 1.Code Review践行“三人行必有我师”
有时候你可能会觉得团队中的资深员工或者技术leader的技术比较牛写的代码很好他们的代码就不需要Review了我们重点Review资历浅的员工的代码就可以了。实际上这种认识是不对的。
我们都知道Google工程师的平均研发水平都很高但即便如此我们发现不管谁提交的代码包括Jeff Dean的只要需要Review都会收到很多comments修改意见。中国有句老话“三人行必有我师”我觉得用在这里非常合适。即便自己觉得写得已经很好的代码只要经过不停地推敲都有持续改进的空间。
所以永远不要觉得自己很厉害写的代码就不需要别人Review了永远不要觉得自己水平很一般就没有资格给别人Review了更不要觉得技术大牛让你Review代码只是缺少你的一个“approve”随便看看就可以。
### 2.Code Review能摒弃“个人英雄主义”
在一个成熟的公司里,所有的架构设计、实现,都应该是一个团队的产出。尽管这个过程可能会由某个人来主导,但应该是整个团队共同智慧的结晶。
如果一个人默默地写代码提交不经过团队的Review这样的代码蕴含的是一个人的智慧。代码的质量完全依赖于这个人的技术水平。这就会导致代码质量参差不齐。如果经过团队多人Review、打磨代码蕴含的是整个团队的智慧可以保证代码按照团队中的最高水准输出。
### 3.Code Review能有效提高代码可读性
前面我们反复强调在大部分情况下代码的可读性比任何其他方面比如扩展性等都重要。可读性好代表后期维护成本低线上bug容易排查新人容易熟悉代码老人离职时代码容易接手。而且可读性好也说明代码足够简单出错可能性小、bug少。
不过自己看自己写的代码总是会觉得很易读但换另外一个人来读你的代码他可能就不这么认为了。毕竟自己写的代码其中涉及的业务、技术自己很熟悉别人不一定会熟悉。既然自己对可读性的判断很容易出现错觉那Code Review就是一种考察代码可读性的很好手段。如果代码审查者很费劲才能看懂你写的代码那就说明代码的可读性有待提高了。
还有不知道你有没有这样的感受写代码的时候时间一长改动的文件一多就感觉晕乎乎的脑子不清醒逻辑不清晰有句话讲“旁观者清当局者迷”说的就是这个意思。Code Review能有效地解决“当局者迷”的问题。在正式开始Code Review之前当我们将代码提交到Review BoardCode Review的工具界面之后所有的代码改动都放到了一块看起来一目了然、清晰可见。这个时候还没有等其他同事Review我们自己就能发现很多问题。
### 4.Code Review是技术传帮带的有效途径
良好的团队需要技术和业务的“传帮带”那如何来做“传帮带”呢当然业务上面我们可能通过文档或口口相传的方式那技术呢如何培养初级工程师的技术能力呢Code Review就是一种很好的途径。每次Code Review都是一次真实案例的讲解。通过Code Review在实践中将技术传递给初级工程师比让他们自己学习、自己摸索来得更高效
### 5.Code Review保证代码不止一个人熟悉
如果一段代码只有一个人熟悉如果这个同事休假了或离职了代码交接起来就比较费劲。有时候我们单纯只看代码还看不大懂又要跟PM、业务团队、或者其他技术团队再重复来一轮沟通搞的其他团队的人都很烦。而Code Review能保证任何代码同时都至少有两个同事熟悉互为备份有备无患除非两个同事同时都离职……
### 6.Code Review能打造良好的技术氛围
提交代码Review的人希望自己写的代码足够优秀毕竟被同事Review出很多问题是件很丢人的事情。而做Code review的人也希望自己尽可能地提出有建设性意见展示自己的能力。所以Code Review还能增进技术交流活跃技术氛围培养大家的极客精神以及对代码质量的追求。
一个良好的技术氛围能让团队有很强的自驱力。不用技术leader反复强调代码质量有多重要团队中的成员就会自己主动去关注代码质量的问题。这比制定各种规章制度、天天督促执行要更加有效。实际上我多说一句好的技术氛围也能降低团队的离职率。
### 7.Code Review是一种技术沟通方式
Talk is cheapshow me the code。怎么“show”通过Code Review工具来“show”这样也方便别人反馈意见。特别是对于跨不同办公室、跨时区的沟通Code Review是一种很好的沟通方式。我今天白天写的代码明天来上班的时候跨时区的同事已经帮我Review好了我就可以改改提交继续写新的代码了。这样的协作效率会很高。
### 8.Code Review能提高团队的自律性
在开发过程中难免会有人不自律存在侥幸心理反正我写的代码也没人看随便写写就提交了。Code Review相当于一次代码直播曝光dirty code有一定的威慑力。这样大家就不敢随便应付一下就提交代码了。
## 如何在团队中落地执行Code Review
刚刚讲了这么多Code Review的好处我觉得大部分你应该都能认可但我猜你可能会说Google之所以能很好地执行Code Review一方面是因为有经验的传承起步阶段已经过去了另一方面是本身员工技术素质、水平就很高那在一个技术水平没那么强的团队在起步阶段或项目工期很紧的情况下如何落地执行Code Review呢
接下来我就很多人关于Code Review的一些疑惑谈谈我自己的看法。
**有人认为Code Review流程太长太浪费时间特别是工期紧的时候今天改的代码明天就要上如果要等同事Review同事有可能没时间这样就来不及。这个时候该怎么办呢**
我所经历的项目还没有一个因为工期紧导致没有时间Code Review的。工期都是人排的稍微排松点就行了啊。我觉得关键还是在于整个公司对Code Review的接受程度。而且Code Review熟练之后并不需要花费太长的时间。尽管开始做Code Review的时候你可能因为不熟练需要有一个checklist对照着来做。起步阶段可能会比较耗时。但当你熟练之后Code Review就像键盘盲打一样你已经忘记了哪个手指按的是哪个键了扫一遍代码就能揪出绝大部分问题。
**有人认为业务一直在变今天写的代码明天可能就要再改代码可能不会长期维护写得太好也没用。这种情况下是不是就不需要Code Review了呢**
这种现象在游戏开发、一些早期的创业公司或者项目验证阶段比较常见。项目讲求短平快先验证产品再优化技术。如果确实面对的还只是生存问题代码质量确实不是首要的特殊情况下不做Code Review是支持的
有人说团队成员技术水平不高过往也没有Code Review的经验不知道Review什么也Review不出什么。自己代码都没写明白不知道什么样的代码是好的什么样的代码是差的更不要说Review别人的代码了。在Code Review的时候团队成员大眼瞪小眼只能Review点语法形式大于效果。这种情况该怎么办
这种情况也挺常见。不过没关系团队的技术水平都是可以培养的。我们可以先让资深同事、技术好的同事或技术leader来Review其他所有人的代码。Review的过程本身就是一种“传帮带”的过程。慢慢地整个团队就知道该如何Review了。虽然这可能会有一个相当长的过程但如果真的想在团队中执行Code Review这不失为一种“曲线救国”的方法。
**还有人说刚开始Code Review的时候大家都还挺认真但时间长了大家觉得这事跟KPI无关而且我还要看别人的代码理解别人写的代码的业务多浪费时间啊。慢慢地Code Review就变得流于形式了。有人提交了代码随便抓个人Review。Review的人也不认真随便扫一眼就点“approve”。这种情况该如何应对**
我的对策是这样的。首先要明确的告诉Code Review的重要性要严格执行让大家不要懈怠适当的时候可以“杀鸡儆猴”。其次可以像Google一样将Code Review间接地跟KPI、升职等联系在一块高级工程师有义务做Code Review就像有义务做技术面试一样。再次想办法活跃团队的技术氛围把Code Review作为一种展示自己技术的机会带动起大家对Code Review的积极性提高大家对Code Review的认同感。
最后我再多说几句。Google的Code Review是做得很好的可以说是谷歌保持代码高质量最有效的手段之一了。Google的Code Review非常严格多一个空行多一个空格注释有拼错的单词变量命名得不够好都会被指出来要求修改。之所以如此吹毛求疵并非矫枉过正而是要给大家传递一个信息代码质量非常重要一点都不能马虎。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天我们主要讲了为什么要做Code ReviewCode Review的价值在哪里。我的总结如下Code Review践行“三人行必有我师”、能摒弃“个人英雄主义”、能有效提高代码可读性、是技术传帮带的有效途径、能保证代码不止一个人熟悉、能打造良好的技术氛围、是一种技术沟通方式、能提高团队的自律性。
除此之外我还对Code Review在落地执行过程中的一些问题做了简单的答疑。我这里就不再重复罗列了。如果你在Code Review过程中遇到同样的问题希望我的建议对你有所帮助。
## 课堂讨论
对是否应该做Code Review你有什么看法呢你所在的公司是否有严格的Code Review呢在Code Review的过程中你又遇到了哪些问题
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,87 @@
<audio id="audio" title="81 | 开源实战三借Google Guava学习发现和开发通用功能模块" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/01/9270563dc26c6f39b8143b03a7cdac01.mp3"></audio>
上几节课我们拿Unix这个超级大型开源软件的开发作为引子从代码设计编写和研发管理两个角度讲了如何应对大型复杂项目的开发。接下来我们再讲一下Google开源的Java开发库Google Guava。
Google Guava是一个非常成功、非常受欢迎的开源项目。它在GitHub上由近3.7万的stars。在Java项目开发中应用很广泛。当然我们并不会讲解其中的每个类、接口如何使用而是重点讲解其背后蕴含的设计思想、使用的设计模式。内容比较多我分三节课来讲解。
- 第一节课我们对Google Guava做一个简单介绍并借此讲一下如何开发一个通用的功能模块。
- 第二节课我们讲Google Guava中用到的几种设计模式会补充讲解之前没有讲到的Immutable模式。
- 第三节课我们借Google Guava补充讲解三大编程范式中的最后一个函数式编程。
话不多说,让我们正式开始今天的学习吧!
## Google Guava介绍
考虑到你可能不熟悉Google Guava我先对它做下简单的介绍。
Google Guava是Google公司内部Java开发工具库的开源版本。Google内部的很多Java项目都在使用它。它提供了一些JDK没有提供的功能以及对JDK已有功能的增强功能。其中就包括集合Collections、缓存Caching、原生类型支持Primitives Support、并发库Concurrency Libraries、通用注解Common Annotation、字符串处理Strings Processing、数学计算Math、I/O、事件总线EventBus等等。
我截取了Google Guava的包结构图贴到了这里你看起来更加直观些。
<img src="https://static001.geekbang.org/resource/image/1c/45/1ce23ffd03045dadf2bad7e126337045.png" alt="">
我们知道JDK的全称是Java Development Kit。它本身就是Java提供的工具类库。那现在请你思考一下既然有了JDK为什么Google还要开发一套新的类库Google Guava是否是重复早轮子两者的差异化在哪里
带着这个问题结合Google Guava我们来学习如何在业务开发中发现通用的功能模块以及如何将它开发成类库、框架或者功能组件。等到学习完之后我希望你能自己回答这个问题。
## 如何发现通用的功能模块?
很多人觉得做业务开发没有挑战实际上做业务开发也会涉及很多非业务功能的开发比如我们前面讲到的ID生成器、性能计数器、EventBus、DI容器以及后面会讲到的限流框架、幂等框架、灰度组件。关键在于我们要有善于发现、善于抽象的能力并且具有扎实的设计、开发能力能够发现这些非业务的、可复用的功能点并且从业务逻辑中将其解耦抽象出来设计并开发成独立的功能模块。
在我看来在业务开发中跟业务无关的通用功能模块常见的一般有三类类库library、框架framework、功能组件component等。
其中Google Guava属于类库提供一组API接口。EventBus、DI容器属于框架提供骨架代码能让业务开发人员聚焦在业务开发部分在预留的扩展点里填充业务代码。ID生成器、性能计数器属于功能组件提供一组具有某一特殊功能的API接口有点类似类库但更加聚焦和重量级比如ID生成器有可能会依赖Redis等外部系统不像类库那么简单。
前面提到的限流、幂等、灰度到底是属于框架还是功能组件我们要视具体情况而定。如果业务代码嵌套在它们里面开发那就可以称它们为框架。如果它们只是开放API接口供业务系统调用那就可以称它们为组件。不过叫什么没有太大关系不必太深究概念。
那我们如何发现项目中的这些通用的功能模块呢?
实际上不管是类库、框架还是功能组件这些通用功能模块有两个最大的特点复用和业务无关。Google Guava就是一个典型的例子。
如果没有复用场景,那也就没有了抽离出来,设计成独立模块的必要了。如果与业务有关又可复用,大部分情况下会设计成独立的系统(比如微服务),而不是类库、框架或功能组件。所以,如果你负责开发的代码,与业务无关并且可能会被复用,那你就可以考虑将它独立出来,开发成类库、框架、功能组件等通用功能模块。
稍微补充一下我们这里讲的是在业务开发中如何发现通用的功能模块。除了业务开发团队之外很多公司还有一些基础架构团队、架构开发团队他们除了开发类库、框架、功能组件之外也会开发一些通用的系统、中间件比如Google MapReduce、Kafka消息中间件、监控系统、分布式调用链追踪系统等。
## 如何开发通用的功能模块?
当我们发现了通用功能模块的开发需求之后如何将它设计开发成一个优秀的类库、框架或功能组件呢今天我们不讲具体的开发技巧具体的开发技巧在后面Spring开源实战那部分我们会讲到一些我今天打算先讲一些更普适的开发思想。我觉得先有了这些你应该更容易理解后面的内容。
作为通用的类库、框架、功能组件,我们希望开发出来之后,不仅仅是自己项目使用,还能用在其他团队的项目中,甚至可以开源出来供更多人所用,这样才能发挥它更大的价值,构建自己的影响力。
所以,对于这些类库、框架、功能组件的开发,我们不能闭门造车,要把它们当作“产品”来开发。这个产品是一个“技术产品”,我们的目标用户是“程序员”,解决的是他们的“开发痛点”。我们要多换位思考,站在用户的角度上,来想他们到底想要什么样的功能。
对于一个技术产品来说尽管Bug少、性能好等技术指标至关重要但是否易用、易集成、易插拔、文档是否全面、是否容易上手等这些产品素质也非常重要甚至还能起到决定性作用。往往就是这些很容易忽视、不被重视的东西会决定一个技术产品是否能在众多的同类中脱颖而出。
具体到Google Guava它是一个开发类库目标用户是Java开发工程师解决用户主要痛点是相对于JDK提供更多的工具类简化代码编写比如它提供了用来判断null值的Preconditions类Splitter、Joiner、CharMatcher字符串处理类Multisets、Multimaps、Tables等更丰富的Collections类等等。
它的优势有这样几点第一由Google管理、长期维护经过充分的单元测试代码质量有保证第二可靠、性能好、高度优化比如Google Guava提供的Immutable Collections要比JDK的unmodifiableCollection性能好第三全面、完善的文档容易上手学习成本低你可以去看下它的Github Wiki。
刚刚讲的是“产品意识”,我们再来讲讲“服务意识”。我经常在团队中说,如果你开发的东西是提供给其他团队用的,你一定要有“服务意识”。对于程序员来说,这点可能比“产品意识”更加欠缺。
首先,从心态上,别的团队使用我们开发出来的技术产品,我们要学会感谢。这点很重要。心态不同了,做起事来就会有微妙的不同。其次,除了写代码,我们还要有抽出大量时间答疑、充当客服角色的心理准备。有了这个心理准备,别的团队的人在问你问题的时候,你也就不会很烦了。
相对于业务代码来说开发这种被多处复用的通用代码对代码质量的要求更高些因为这些项目的影响面更大一旦出现bug会牵连很多系统或其他项目。特别是如果你要把项目开源影响就更大了。所以这类项目的代码质量一般都很好开发这类项目对代码能力的锻炼更有大。这也是我经常推荐别人通过阅读著名开源项目代码、参与开源项目来提高技术的原因。
具体到Google Guava它是Google员工开发的单元测试很完善注释写得很规范代码写得也很好可以说是学习Google开发经验的一手资料建议你如果有时间的话可以认真阅读一下它的代码。
尽管开发这些通用功能模块更加锻炼技术,但我们也不要重复造轮子,能复用的尽量复用。而且,在项目中,如果你想把所有的通用功能都开发为独立的类库、框架、功能组件,这就有点大动干戈了,有可能会得不到领导的支持。毕竟从项目中将这部分通用功能独立出来开发,比起作为项目的一部分来开发,会更加耗时。
所以,权衡一下的话,我建议初期先把这些通用的功能作为项目的一部分来开发。不过,在开发的时候,我们做好模块化工作,将它们尽量跟其他模块划清界限,通过接口、扩展点等松耦合的方式跟其他模式交互。等到时机成熟了,我们再将它从项目中剥离出来。因为之前模块化做的好,耦合程度低,剥离出来的成本也就不会很高。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
做业务开发也会涉及很多非业务功能的开发。我们要有善于发现、善于抽象的能力,并且具有扎实的设计、开发能力,能够发现这些非业务的、可复用的功能点,并且从业务逻辑中将其解耦抽象出来,设计并开发成独立的功能模块,比如类库、框架、功能组件。
实际上,不管是类库、框架还是功能组件,这些通用功能模块最大的两个特点就是复用和业务无关。如果你开发的这块代码,业务无关并且可能会被复用,那就可以考虑将它独立出来,开发成类库、框架、功能组件等。
当我们发现了通用功能模块的开发需求之后,如何将它设计开发成一个优秀的类库、框架或功能组件呢?这里我们讲了一些更普适的开发思想,比如产品意识、服务意识、代码质量意识、不要重复造轮子等。
除此之外我特别建议你去阅读一下Google Guava的开源代码。它的代码不复杂很容易读懂不会有太大阅读负担但它是你获取Google公司开发经验的一手资料特别是在单元测试、编码规范方面。
## 课堂讨论
针对你正在参与开发的项目,思考一下,有哪些通用的功能模块可以抽象出来,设计开发成独立的类库、框架、功能组件?它们都可能会包括哪些功能点呢?试着自己设计一下吧!
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,332 @@
<audio id="audio" title="82 | 开源实战三剖析Google Guava中用到的几种设计模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1a/16/1a837635ee15f01f9d7200bb2f2f4a16.mp3"></audio>
上一节课我们通过Google Guava这样一个优秀的开源类库讲解了如何在业务开发中发现跟业务无关、可以复用的通用功能模块并将它们从业务代码中抽离出来设计开发成独立的类库、框架或功能组件。
今天我们再来学习一下Google Guava中用到的几种经典设计模式Builder模式、Wrapper模式以及之前没讲过的Immutable模式。
话不多说,让我们正式开始今天的学习吧!
## Builder模式在Guava中的应用
在项目开发中,我们经常用到缓存。它可以非常有效地提高访问速度。
常用的缓存系统有Redis、Memcache等。但是如果要缓存的数据比较少我们完全没必要在项目中独立部署一套缓存系统。毕竟系统都有一定出错的概率项目中包含的系统越多那组合起来项目整体出错的概率就会升高可用性就会降低。同时多引入一个系统就要多维护一个系统项目维护的成本就会变高。
取而代之我们可以在系统内部构建一个内存缓存跟系统集成在一起开发、部署。那如何构建内存缓存呢我们可以基于JDK提供的类比如HashMap从零开始开发内存缓存。不过从零开发一个内存缓存涉及的工作就会比较多比如缓存淘汰策略等。为了简化开发我们就可以使用Google Guava提供的现成的缓存工具类com.google.common.cache.*。
使用Google Guava来构建内存缓存非常简单我写了一个例子贴在了下面你可以看下。
```
public class CacheDemo {
public static void main(String[] args) {
Cache&lt;String, String&gt; cache = CacheBuilder.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
cache.put(&quot;key1&quot;, &quot;value1&quot;);
String value = cache.getIfPresent(&quot;key1&quot;);
System.out.println(value);
}
}
```
从上面的代码中我们可以发现Cache对象是通过CacheBuilder这样一个Builder类来创建的。为什么要由Builder类来创建Cache对象呢我想这个问题应该对你来说没难度了吧。
你可以先想一想然后再来看我的回答。构建一个缓存需要配置n多参数比如过期时间、淘汰策略、最大缓存大小等等。相应地Cache类就会包含n多成员变量。我们需要在构造函数中设置这些成员变量的值但又不是所有的值都必须设置设置哪些值由用户来决定。为了满足这个需求我们就需要定义多个包含不同参数列表的构造函数。
为了避免构造函数的参数列表过长、不同的构造函数过多我们一般有两种解决方案。其中一个解决方案是使用Builder模式另一个方案是先通过无参构造函数创建对象然后再通过setXXX()方法来逐一设置需要的设置的成员变量。
那我再问你一个问题为什么Guava选择第一种而不是第二种解决方案呢使用第二种解决方案是否也可以呢答案是不行的。至于为什么我们看下源码就清楚了。我把CacheBuilder类中的build()函数摘抄到了下面,你可以先看下。
```
public &lt;K1 extends K, V1 extends V&gt; Cache&lt;K1, V1&gt; build() {
this.checkWeightWithWeigher();
this.checkNonLoadingCache();
return new LocalManualCache(this);
}
private void checkNonLoadingCache() {
Preconditions.checkState(this.refreshNanos == -1L, &quot;refreshAfterWrite requires a LoadingCache&quot;);
}
private void checkWeightWithWeigher() {
if (this.weigher == null) {
Preconditions.checkState(this.maximumWeight == -1L, &quot;maximumWeight requires weigher&quot;);
} else if (this.strictParsing) {
Preconditions.checkState(this.maximumWeight != -1L, &quot;weigher requires maximumWeight&quot;);
} else if (this.maximumWeight == -1L) {
logger.log(Level.WARNING, &quot;ignoring weigher specified without maximumWeight&quot;);
}
}
```
看了代码你是否有了答案呢实际上答案我们在讲Builder模式的时候已经讲过了。现在我们再结合CacheBuilder的源码重新说下。
必须使用Builder模式的主要原因是在真正构造Cache对象的时候我们必须做一些必要的参数校验也就是build()函数中前两行代码要做的工作。如果采用无参默认构造函数加setXXX()方法的方案这两个校验就无处安放了。而不经过校验创建的Cache对象有可能是不合法、不可用的。
## Wrapper模式在Guava中的应用
在Google Guava的collection包路径下有一组以Forwarding开头命名的类。我截了这些类中的一部分贴到了下面你可以看下。
<img src="https://static001.geekbang.org/resource/image/ac/7d/ac5ce5f711711c0b86149f402e76177d.png" alt="">
这组Forwarding类很多但实现方式都很相似。我摘抄了其中的ForwardingCollection中的部分代码到这里你可以下先看下代码然后思考下这组Forwarding类是干什么用的。
```
@GwtCompatible
public abstract class ForwardingCollection&lt;E&gt; extends ForwardingObject implements Collection&lt;E&gt; {
protected ForwardingCollection() {
}
protected abstract Collection&lt;E&gt; delegate();
public Iterator&lt;E&gt; iterator() {
return this.delegate().iterator();
}
public int size() {
return this.delegate().size();
}
@CanIgnoreReturnValue
public boolean removeAll(Collection&lt;?&gt; collection) {
return this.delegate().removeAll(collection);
}
public boolean isEmpty() {
return this.delegate().isEmpty();
}
public boolean contains(Object object) {
return this.delegate().contains(object);
}
@CanIgnoreReturnValue
public boolean add(E element) {
return this.delegate().add(element);
}
@CanIgnoreReturnValue
public boolean remove(Object object) {
return this.delegate().remove(object);
}
public boolean containsAll(Collection&lt;?&gt; collection) {
return this.delegate().containsAll(collection);
}
@CanIgnoreReturnValue
public boolean addAll(Collection&lt;? extends E&gt; collection) {
return this.delegate().addAll(collection);
}
@CanIgnoreReturnValue
public boolean retainAll(Collection&lt;?&gt; collection) {
return this.delegate().retainAll(collection);
}
public void clear() {
this.delegate().clear();
}
public Object[] toArray() {
return this.delegate().toArray();
}
//...省略部分代码...
}
```
光看ForwardingCollection的代码实现你可能想不到它的作用。我再给点提示举一个它的用法示例如下所示
```
public class AddLoggingCollection&lt;E&gt; extends ForwardingCollection&lt;E&gt; {
private static final Logger logger = LoggerFactory.getLogger(AddLoggingCollection.class);
private Collection&lt;E&gt; originalCollection;
public AddLoggingCollection(Collection&lt;E&gt; originalCollection) {
this.originalCollection = originalCollection;
}
@Override
protected Collection delegate() {
return this.originalCollection;
}
@Override
public boolean add(E element) {
logger.info(&quot;Add element: &quot; + element);
return this.delegate().add(element);
}
@Override
public boolean addAll(Collection&lt;? extends E&gt; collection) {
logger.info(&quot;Size of elements to add: &quot; + collection.size());
return this.delegate().addAll(collection);
}
}
```
结合源码和示例我想你应该知道这组Forwarding类的作用了吧
在上面的代码中AddLoggingCollection是基于代理模式实现的一个代理类它在原始Collection类的基础之上针对“add”相关的操作添加了记录日志的功能。
我们前面讲到代理模式、装饰器、适配器模式可以统称为Wrapper模式通过Wrapper类二次封装原始类。它们的代码实现也很相似都可以通过组合的方式将Wrapper类的函数实现委托给原始类的函数来实现。
```
public interface Interf {
void f1();
void f2();
}
public class OriginalClass implements Interf {
@Override
public void f1() { //... }
@Override
public void f2() { //... }
}
public class WrapperClass implements Interf {
private OriginalClass oc;
public WrapperClass(OriginalClass oc) {
this.oc = oc;
}
@Override
public void f1() {
//...附加功能...
this.oc.f1();
//...附加功能...
}
@Override
public void f2() {
this.oc.f2();
}
}
```
实际上这个ForwardingCollection类是一个“默认Wrapper类”或者叫“缺省Wrapper类”。这类似于在装饰器模式那一节课中讲到的FilterInputStream缺省装饰器类。你可以再重新看下[第50讲](https://time.geekbang.org/column/article/204845)装饰器模式的相关内容。
如果我们不使用这个ForwardinCollection类而是让AddLoggingCollection代理类直接实现Collection接口那Collection接口中的所有方法都要在AddLoggingCollection类中实现一遍而真正需要添加日志功能的只有add()和addAll()两个函数其他函数的实现都只是类似Wrapper类中f2()函数的实现那样简单地委托给原始collection类对象的对应函数。
为了简化Wrapper模式的代码实现Guava提供一系列缺省的Forwarding类。用户在实现自己的Wrapper类的时候基于缺省的Forwarding类来扩展就可以只实现自己关心的方法其他不关心的方法使用缺省Forwarding类的实现就像AddLoggingCollection类的实现那样。
## Immutable模式在Guava中的应用
Immutable模式中文叫作不变模式它并不属于经典的23种设计模式但作为一种较常用的设计思路可以总结为一种设计模式来学习。之前在理论部分我们只稍微提到过Immutable模式但没有独立的拿出来详细讲解我们这里借Google Guava再补充讲解一下。
一个对象的状态在对象创建之后就不再改变,这就是所谓的不变模式。其中涉及的类就是**不变类**Immutable Class对象就是**不变对象**Immutable Object。在Java中最常用的不变类就是String类String对象一旦创建之后就无法改变。
不变模式可以分为两类一类是普通不变模式另一类是深度不变模式Deeply Immutable Pattern。普通的不变模式指的是对象中包含的引用对象是可以改变的。如果不特别说明通常我们所说的不变模式指的就是普通的不变模式。深度不变模式指的是对象包含的引用对象也不可变。它们两个之间的关系有点类似之前讲过的浅拷贝和深拷贝之间的关系。我举了一个例子来进一步解释一下代码如下所示
```
// 普通不变模式
public class User {
private String name;
private int age;
private Address addr;
public User(String name, int age, Address addr) {
this.name = name;
this.age = age;
this.addr = addr;
}
// 只有getter方法无setter方法...
}
public class Address {
private String province;
private String city;
public Address(String province, String city) {
this.province = province;
this.city= city;
}
// 有getter方法也有setter方法...
}
// 深度不变模式
public class User {
private String name;
private int age;
private Address addr;
public User(String name, int age, Address addr) {
this.name = name;
this.age = age;
this.addr = addr;
}
// 只有getter方法无setter方法...
}
public class Address {
private String province;
private String city;
public Address(String province, String city) {
this.province = province;
this.city= city;
}
// 只有getter方法无setter方法..
}
```
在某个业务场景下如果一个对象符合创建之后就不会被修改这个特性那我们就可以把它设计成不变类。显式地强制它不可变这样能避免意外被修改。那如何将一个类设置为不变类呢其实方法很简单只要这个类满足所有的成员变量都通过构造函数一次性设置好不暴露任何set等修改成员变量的方法。除此之外因为数据不变所以不存在并发读写问题因此不变模式常用在多线程环境下来避免线程加锁。所以不变模式也常被归类为多线程设计模式。
接下来我们来看一种特殊的不变类那就是不变集合。Google Guava针对集合类Collection、List、Set、Map…提供了对应的不变集合类ImmutableCollection、ImmutableList、ImmutableSet、ImmutableMap…。刚刚我们讲过不变模式分为两种普通不变模式和深度不变模式。Google Guava提供的不变集合类属于前者也就是说集合中的对象不会增删但是对象的成员变量或叫属性值是可以改变的。
实际上Java JDK也提供了不变集合类UnmodifiableCollection、UnmodifiableList、UnmodifiableSet、UnmodifiableMap…。那它跟Google Guava提供的不变集合类的区别在哪里呢我举个例子你就明白了代码如下所示
```
public class ImmutableDemo {
public static void main(String[] args) {
List&lt;String&gt; originalList = new ArrayList&lt;&gt;();
originalList.add(&quot;a&quot;);
originalList.add(&quot;b&quot;);
originalList.add(&quot;c&quot;);
List&lt;String&gt; jdkUnmodifiableList = Collections.unmodifiableList(originalList);
List&lt;String&gt; guavaImmutableList = ImmutableList.copyOf(originalList);
//jdkUnmodifiableList.add(&quot;d&quot;); // 抛出UnsupportedOperationException
// guavaImmutableList.add(&quot;d&quot;); // 抛出UnsupportedOperationException
originalList.add(&quot;d&quot;);
print(originalList); // a b c d
print(jdkUnmodifiableList); // a b c d
print(guavaImmutableList); // a b c
}
private static void print(List&lt;String&gt; list) {
for (String s : list) {
System.out.print(s + &quot; &quot;);
}
System.out.println();
}
}
```
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天我们学习了Google Guava中都用到的几个设计模式Builder模式、Wrapper模式、Immutable模式。还是那句话内容本身不重要你也不用死记硬背Google Guava的某某类用到了某某设计模式。实际上我想通过这些源码的剖析传达给你下面这些东西。
我们在阅读源码的时候,要问问自己,为什么它要这么设计?不这么设计行吗?还有更好的设计吗?实际上,很多人缺少这种“质疑”精神,特别是面对权威(经典书籍、著名源码、权威人士)的时候。
我觉得我本人是最不缺质疑精神的一个人我喜欢挑战权威喜欢以理服人。就好比在今天的讲解中我把ForwardingCollection等类理解为缺省Wrapper类可以用在装饰器、代理、适配器三种Wrapper模式中简化代码编写。如果你去看Google Guava在GitHub上的Wiki你会发现它对ForwardingCollection类的理解跟我是不一样的。它把ForwardingCollection类单纯地理解为缺省的装饰器类只用在装饰器模式中。我个人觉得我的理解更加好些不知道你怎么认为呢
除此之外在专栏的最开始我也讲到学习设计模式能让你更好的阅读源码、理解源码。如果我们没有之前的理论学习那对于很多源码的阅读可能都只停留在走马观花的层面上根本学习不到它的精髓。这就好比今天讲到的CacheBuilder。我想大部分人都知道它是利用了Builder模式但如果对Builder模式没有深入的了解很少人能讲清楚为什么要用Builder模式不用构造函数加set方法的方式来实现。
## 课堂讨论
从最后一段代码中我们可以发现JDK不变集合和Google Guava不变集合都不可增删数据。但是当原始集合增加数据之后JDK不变集合的数据随之增加而Google Guava的不变集合的数据并没有增加。这是两者最大的区别。那这两者底层分别是如何实现不变的呢
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,267 @@
<audio id="audio" title="83 | 开源实战三借Google Guava学习三大编程范式中的函数式编程" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b9/38/b90d62e83b62f7b247e7d887c8496938.mp3"></audio>
现在主流的编程范式主要有三种,面向过程、面向对象和函数式编程。在理论部分,我们已经详细讲过前两种了。今天,我们再借机会讲讲剩下的一种,函数式编程。
函数式编程并非一个很新的东西早在50多年前就已经出现了。近几年函数式编程越来越被人关注出现了很多新的函数式编程语言比如Clojure、Scala、Erlang等。一些非函数式编程语言也加入了很多特性、语法、类库来支持函数式编程比如Java、Python、Ruby、JavaScript等。除此之外Google Guava也有对函数式编程的增强功能。
函数式编程因其编程的特殊性,仅在科学计算、数据处理、统计分析等领域,才能更好地发挥它的优势,所以,我个人觉得,它并不能完全替代更加通用的面向对象编程范式。但是,作为一种补充,它也有很大存在、发展和学习的意义。所以,我觉得有必要在专栏里带你一块学习一下。
话不多说,让我们正式开始今天的学习吧!
## 到底什么是函数式编程?
函数式编程的英文翻译是Functional Programming。 那到底什么是函数式编程呢?
在前面的章节中,我们讲到,面向过程、面向对象编程并没有严格的官方定义。在当时的讲解中,我也只是给出了我自己总结的定义。而且,当时给出的定义也只是对两个范式主要特性的总结,并不是很严格。实际上,函数式编程也是如此,也没有一个严格的官方定义。所以,接下来,我就从特性上来告诉你,什么是函数式编程。
严格上来讲函数式编程中的“函数”并不是指我们编程语言中的“函数”概念而是指数学“函数”或者“表达式”比如y=f(x))。不过,在编程实现的时候,对于数学“函数”或“表达式”,我们一般习惯性地将它们设计成函数。所以,如果不深究的话,函数式编程中的“函数”也可以理解为编程语言中的“函数”。
每个编程范式都有自己独特的地方,这就是它们会被抽象出来作为一种范式的原因。面向对象编程最大的特点是:以类、对象作为组织代码的单元以及它的四大特性。面向过程编程最大的特点是:以函数作为组织代码的单元,数据与方法相分离。那函数式编程最独特的地方又在哪里呢?
实际上,函数式编程最独特的地方在于它的编程思想。函数式编程认为,程序可以用一系列数学函数或表达式的组合来表示。函数式编程是程序面向数学的更底层的抽象,将计算过程描述为表达式。不过,这样说你肯定会有疑问,真的可以把任何程序都表示成一组数学表达式吗?
理论上讲是可以的。但是,并不是所有的程序都适合这么做。函数式编程有它自己适合的应用场景,比如开篇提到的科学计算、数据处理、统计分析等。在这些领域,程序往往比较容易用数学表达式来表示,比起非函数式编程,实现同样的功能,函数式编程可以用很少的代码就能搞定。但是,对于强业务相关的大型业务系统开发来说,费劲吧啦地将它抽象成数学表达式,硬要用函数式编程来实现,显然是自讨苦吃。相反,在这种应用场景下,面向对象编程更加合适,写出来的代码更加可读、可维护。
刚刚讲的是函数式编程的编程思想,如果我们再具体到编程实现,函数式编程跟面向过程编程一样,也是以函数作为组织代码的单元。不过,它跟面向过程编程的区别在于,它的函数是无状态的。何为无状态?简单点讲就是,函数内部涉及的变量都是局部变量,不会像面向对象编程那样,共享类成员变量,也不会像面向过程编程那样,共享全局变量。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果都是一样的。这实际上就是数学函数或数学表达式的基本要求。我举个例子来简单解释一下。
```
// 有状态函数: 执行结果依赖b的值是多少即便入参相同多次执行函数函数的返回值有可能不同因为b值有可能不同。
int b;
int increase(int a) {
return a + b;
}
// 无状态函数:执行结果不依赖任何外部变量值,只要入参相同,不管执行多少次,函数的返回值就相同
int increase(int a, int b) {
return a + b;
}
```
这里稍微总结一下不同的编程范式之间并不是截然不同的总是有一些相同的编程规则。比如不管是面向过程、面向对象还是函数式编程它们都有变量、函数的概念最顶层都要有main函数执行入口来组装编程单元类、函数等。只不过面向对象的编程单元是类或对象面向过程的编程单元是函数函数式编程的编程单元是无状态函数。
## Java对函数式编程的支持
我们前面讲到,实现面向对象编程不一定非得使用面向对象编程语言,同理,实现函数式编程也不一定非得使用函数式编程语言。现在,很多面向对象编程语言,也提供了相应的语法、类库来支持函数式编程。
接下来我们就看下Java这种面向对象编程语言对函数式编程的支持借机加深一下你对函数式编程的理解。我们先来看下面这样一段非常典型的Java函数式编程的代码。
```
public class FPDemo {
public static void main(String[] args) {
Optional&lt;Integer&gt; result = Stream.of(&quot;f&quot;, &quot;ba&quot;, &quot;hello&quot;)
.map(s -&gt; s.length())
.filter(l -&gt; l &lt;= 3)
.max((o1, o2) -&gt; o1-o2);
System.out.println(result.get()); // 输出2
}
}
```
这段代码的作用是从一组字符串数组中过滤出长度小于等于3的字符串并且求得这其中的最大长度。
如果你不了解Java函数式编程的语法看了上面的代码或许会有些懵主要的原因是Java为函数式编程引入了三个新的语法概念Stream类、Lambda表达式和函数接口Functional Inteface。Stream类用来支持通过“.”级联多个函数操作的代码编写方式引入Lambda表达式的作用是简化代码编写函数接口的作用是让我们可以把函数包裹成函数接口来实现把函数当做参数一样来使用Java不像C一样支持函数指针可以把函数直接当参数来使用
**首先我们来看下Stream类。**
假设我们要计算这样一个表达式:(3-1)*2+5。如果按照普通的函数调用的方式写出来就是下面这个样子
```
add(multiply(subtract(3,1),2),5);
```
不过,这样编写代码看起来会比较难理解,我们换个更易读的写法,如下所示:
```
subtract(3,1).multiply(2).add(5);
```
我们知道在Java中“.”表示调用某个对象的方法。为了支持上面这种级联调用方式我们让每个函数都返回一个通用的类型Stream类对象。在Stream类上的操作有两种中间操作和终止操作。中间操作返回的仍然是Stream类对象而终止操作返回的是确定的值结果。
我们再来看之前的例子。我对代码做了注释解释如下所示。其中map、filter是中间操作返回Stream类对象可以继续级联其他操作max是终止操作返回的不是Stream类对象无法再继续往下级联处理了。
```
public class FPDemo {
public static void main(String[] args) {
Optional&lt;Integer&gt; result = Stream.of(&quot;f&quot;, &quot;ba&quot;, &quot;hello&quot;) // of返回Stream&lt;String&gt;对象
.map(s -&gt; s.length()) // map返回Stream&lt;Integer&gt;对象
.filter(l -&gt; l &lt;= 3) // filter返回Stream&lt;Integer&gt;对象
.max((o1, o2) -&gt; o1-o2); // max终止操作返回Optional&lt;Integer&gt;
System.out.println(result.get()); // 输出2
}
}
```
**其次我们再来看下Lambda表达式。**
我们前面讲到Java引入Lambda表达式的主要作用是简化代码编写。实际上我们也可以不用Lambda表达式来书写例子中的代码。我们拿其中的map函数来举例说明一下。
下面有三段代码第一段代码展示了map函数的定义实际上map函数接收的参数是一个Function接口也就是待会儿要讲到的函数接口。第二段代码展示了map函数的使用方式。第三段代码是针对第二段代码用Lambda表达式简化之后的写法。实际上Lambda表达式在Java中只是一个语法糖而已底层是基于函数接口来实现的也就是第二段代码展示的写法。
```
// Stream中map函数的定义
public interface Stream&lt;T&gt; extends BaseStream&lt;T, Stream&lt;T&gt;&gt; {
&lt;R&gt; Stream&lt;R&gt; map(Function&lt;? super T, ? extends R&gt; mapper);
//...省略其他函数...
}
// Stream中map的使用方法
Stream.of(&quot;fo&quot;, &quot;bar&quot;, &quot;hello&quot;).map(new Function&lt;String, Integer&gt;() {
@Override
public Integer apply(String s) {
return s.length();
}
});
// 用Lambda表达式简化后的写法
Stream.of(&quot;fo&quot;, &quot;bar&quot;, &quot;hello&quot;).map(s -&gt; s.length());
```
Lambda表达式语法不是我们学习的重点。我这里只稍微介绍一下。如果感兴趣你可以自行深入研究。
Lambda表达式包括三部分输入、函数体、输出。表示出来的话就是下面这个样子
```
(a, b) -&gt; { 语句1 语句2...; return 输出; } //a,b是输入参数
```
实际上Lambda表达式的写法非常灵活。我们刚刚给出的是标准写法还有很多简化写法。比如如果输入参数只有一个可以省略()直接写成a-&gt;{…};如果没有入参,可以直接将输入和箭头都省略掉,只保留函数体;如果函数体只有一个语句,那可以将{}省略掉如果函数没有返回值return语句就可以不用写了。
如果我们把之前例子中的Lambda表达式全部替换为函数接口的实现方式就是下面这样子的。代码是不是多了很多
```
Optional&lt;Integer&gt; result = Stream.of(&quot;f&quot;, &quot;ba&quot;, &quot;hello&quot;)
.map(s -&gt; s.length())
.filter(l -&gt; l &lt;= 3)
.max((o1, o2) -&gt; o1-o2);
// 还原为函数接口的实现方式
Optional&lt;Integer&gt; result2 = Stream.of(&quot;fo&quot;, &quot;bar&quot;, &quot;hello&quot;)
.map(new Function&lt;String, Integer&gt;() {
@Override
public Integer apply(String s) {
return s.length();
}
})
.filter(new Predicate&lt;Integer&gt;() {
@Override
public boolean test(Integer l) {
return l &lt;= 3;
}
})
.max(new Comparator&lt;Integer&gt;() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
```
**最后,我们来看下函数接口。**
实际上上面一段代码中的Function、Predicate、Comparator都是函数接口。我们知道C语言支持函数指针它可以把函数直接当变量来使用。但是Java没有函数指针这样的语法。所以它通过函数接口将函数包裹在接口中当作变量来使用。
实际上函数接口就是接口。不过它也有自己特别的地方那就是要求只包含一个未实现的方法。因为只有这样Lambda表达式才能明确知道匹配的是哪个接口。如果有两个未实现的方法并且接口入参、返回值都一样那Java在翻译Lambda表达式的时候就不知道表达式对应哪个方法了。
我把Java提供的Function、Predicate这两个函数接口的源码摘抄过来贴到了下面你可以对照着它们理解我刚刚对函数接口的讲解。
```
@FunctionalInterface
public interface Function&lt;T, R&gt; {
R apply(T t); // 只有这一个未实现的方法
default &lt;V&gt; Function&lt;V, R&gt; compose(Function&lt;? super V, ? extends T&gt; before) {
Objects.requireNonNull(before);
return (V v) -&gt; apply(before.apply(v));
}
default &lt;V&gt; Function&lt;T, V&gt; andThen(Function&lt;? super R, ? extends V&gt; after) {
Objects.requireNonNull(after);
return (T t) -&gt; after.apply(apply(t));
}
static &lt;T&gt; Function&lt;T, T&gt; identity() {
return t -&gt; t;
}
}
@FunctionalInterface
public interface Predicate&lt;T&gt; {
boolean test(T t); // 只有这一个未实现的方法
default Predicate&lt;T&gt; and(Predicate&lt;? super T&gt; other) {
Objects.requireNonNull(other);
return (t) -&gt; test(t) &amp;&amp; other.test(t);
}
default Predicate&lt;T&gt; negate() {
return (t) -&gt; !test(t);
}
default Predicate&lt;T&gt; or(Predicate&lt;? super T&gt; other) {
Objects.requireNonNull(other);
return (t) -&gt; test(t) || other.test(t);
}
static &lt;T&gt; Predicate&lt;T&gt; isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -&gt; targetRef.equals(object);
}
}
```
以上讲的就是Java对函数式编程的语法支持我想最开始给到的那个函数式编程的例子现在你应该能轻松看懂了吧
## Guava对函数式编程的增强
如果你是Google Guava的设计者对于Java函数式编程Google Guava还能做些什么呢
颠覆式创新是很难的。不过我们可以进行一些补充一方面可以增加Stream类上的操作类似map、filter、max这样的终止操作和中间操作另一方面也可以增加更多的函数接口类似Function、Predicate这样的函数接口。实际上我们还可以设计一些类似Stream类的新的支持级联操作的类。这样使用Java配合Guava进行函数式编程会更加方便。
但是跟我们预期的相反Google Guava并没有提供太多函数式编程的支持仅仅封装了几个遍历集合操作的接口代码如下所示
```
Iterables.transform(Iterable, Function);
Iterators.transform(Iterator, Function);
Collections.transfrom(Collection, Function);
Lists.transform(List, Function);
Maps.transformValues(Map, Function);
Multimaps.transformValues(Mltimap, Function);
...
Iterables.filter(Iterable, Predicate);
Iterators.filter(Iterator, Predicate);
Collections2.filter(Collection, Predicate);
...
```
从Google Guava的GitHub Wiki中我们发现Google对于函数式编程的使用还是很谨慎的认为过度地使用函数式编程会导致代码可读性变差强调不要滥用。这跟我前面对函数式编程的观点是一致的。所以在函数式编程方面Google Guava并没有提供太多的支持。
之所以对遍历集合操作做了优化主要是因为函数式编程一个重要的应用场景就是遍历集合。如果不使用函数式编程我们只能for循环一个一个的处理集合中的数据。使用函数式编程可以大大简化遍历集合操作的代码编写一行代码就能搞定而且在可读性方面也没有太大损失。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天,我们讲了一下三大编程范式中的最后一个,函数式编程。尽管越来越多的编程语言开始支持函数式编程,但我个人觉得,它只能是其他编程范式的补充,用在一些特殊的领域发挥它的特殊作用,没法完全替代面向对象、面向过程编程范式。
关于什么是函数式编程,实际上不是很好理解。函数式编程中的“函数”,并不是指我们编程语言中的“函数”概念,而是数学中的“函数”或者“表达式”概念。函数式编程认为,程序可以用一系列数学函数或表达式的组合来表示。
具体到编程实现,函数式编程以无状态函数作为组织代码的单元。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果都是一样。
具体到Java语言它提供了三个语法机制来支持函数式编程。它们分别是Stream类、Lambda表达式和函数接口。Google Guava对函数式编程的一个重要应用场景遍历集合做了优化但并没有太多的支持并且我们强调不要为了节省代码行数滥用函数式编程导致代码可读性变差。
## 课堂讨论
你可以说一说函数式编程的优点和缺点,以及你对函数式编程的看法。你觉得它能否替代面向对象编程,成为最主流的编程范式?
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,106 @@
<audio id="audio" title="84 | 开源实战四剖析Spring框架中蕴含的经典设计思想或原则" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fa/81/fa02986ea66c4b22819ced4499311b81.mp3"></audio>
在Java世界里Spring框架已经几乎成为项目开发的必备框架。作为如此优秀和受欢迎的开源项目它是我们源码阅读的首选材料之一不管是设计思想还是代码实现都有很多值得我们学习的地方。接下来我们就详细讲讲Spring框架中蕴含的设计思想、原则和模式。因为内容比较多我分三部分来讲解。
- 第一部分我们讲解Spring框架中蕴含的经典设计思想或原则。
- 第二部分我们讲解Spring框架中用来支持扩展的两种设计模式。
- 第三部分我们总结罗列Spring框架中用到的其他十几种设计模式。
今天我们就讲下第一部分Spring框架中蕴含的一些设计思想或原则这其中就包括约定大于配置、低侵入松耦合、模块化轻量级等。这些设计思想都很通用掌握之后我们可以借鉴用到其他框架的开发中。
话不多少,让我们正式开始今天的学习吧!
## Spring框架简单介绍
考虑到你可能不熟悉Spring我这里对它做下简单介绍。我们常说的Spring框架是指Spring Framework基础框架。Spring Framework是整个Spring生态也被称作Spring全家桶的基石。除了Spring FrameworkSpring全家桶中还有更多基于Spring Framework开发出来的、整合更多功能的框架比如Spring Boot、Spring Cloud。
在Spring全家桶中Spring Framework是最基础、最底层的一部分。它提供了最基础、最核心的IOC和AOP功能。当然它包含的功能还不仅如此还有其他比如事务管理Transactions、MVC框架Spring MVC等很多功能。下面这个表格是我从Spring官网上找的关于Spring Framework的功能介绍你可以大略地看下有个印象。
<img src="https://static001.geekbang.org/resource/image/1a/41/1ab07ad6aed0f06cbce9547552281041.jpeg" alt="">
在Spring Framework中Spring MVC出镜率很高经常被单独拎出来使用。它是支持Web开发的MVC框架提供了URL路由、Session管理、模板引擎等跟Web开发相关的一系列功能。
Spring Boot是基于Spring Framework开发的。它更加专注于微服务开发。之所以名字里带有“Boot”一词跟它的设计初衷有关。Spring Boot的设计初衷是快速启动一个项目利用它可以快速地实现一个项目的开发、部署和运行。Spring Boot支持的所有功能都是围绕着这个初衷设计的比如集成很多第三方开发包、简化配置比如规约优于配置、集成内嵌Web容器比如Tomcat、Jetty等。
单个的微服务开发使用Spring Boot就足够了但是如果要构建整个微服务集群就需要用到Spring Cloud了。Spring Cloud主要负责微服务集群的服务治理工作包含很多独立的功能组件比如Spring Cloud Sleuth调用链追踪、Spring Cloud Config配置中心等。
## 从Spring看框架的作用
如果你使用过一些框架来做开发你应该能感受到使用框架开发的优势。这里我稍微总结一下。利用框架的好处有解耦业务和非业务开发、让程序员聚焦在业务开发上隐藏复杂实现细节、降低开发难度、减少代码bug实现代码复用、节省开发时间规范化标准化项目开发、降低学习和维护成本等等。实际上如果要用一句话来总结一下的话那就是简化开发
对于刚刚的总结,我们再详细解释一下。
相比单纯的CRUD业务代码开发非业务代码开发要更难一些。所以将一些非业务的通用代码开发为框架在项目中复用除了节省开发时间之外也降低了项目开发的难度。除此之外框架经过多个项目的多次验证比起每个项目都重新开发代码的bug会相对少一些。而且不同的项目使用相同的框架对于研发人员来说从一个项目切换到另一个项目的学习成本也会降低很多。
接下来我们再拿常见的Web项目开发来举例说明一下。
通过在项目中引入Spring MVC开发框架开发一个Web应用我们只需要创建Controller、Service、Repository三层类在其中填写相应的业务代码然后做些简单的配置告知框架Controller、Service、Repository类之间的调用关系剩下的非业务相关的工作比如对象的创建、组装、管理请求的解析、封装URL与Controller之间的映射都由框架来完成。
不仅如此如果我们直接引入功能更强大的Spring Boot那将应用部署到Web容器的工作都省掉了。Spring Boot内嵌了Tomcat、Jetty等Web容器。在编写完代码之后我们用一条命令就能完成项目的部署、运行。
## Spring框架蕴含的设计思想
在Google Guava源码讲解中我们讲到开发通用功能模块的一些比较普适的开发思想比如产品意识、服务意识、代码质量意识、不要重复早轮子等。今天我们剖析一下Spring框架背后的一些经典设计思想或开发技巧。这些设计思想并非Spring独有都比较通用能借鉴应用在很多通用功能模块的设计开发中。这也是我们学习Spring源码的价值所在。
### 1.约定优于配置
在使用Spring开发的项目中配置往往会比较复杂、繁琐。比如我们利用Spring MVC来开发Web应用需要配置每个Controller类以及Controller类中的接口对应的URL。
如何来简化配置呢一般来讲有两种方法一种是基于注解另一种是基于约定。这两种配置方式在Spring中都有用到。Spring在最小化配置方面做得淋漓尽致有很多值得我们借鉴的地方。
基于注解的配置方式我们在指定类上使用指定的注解来替代集中的XML配置。比如我们使用@RequestMapping注解在Controller类或者接口上标注对应的URL使用@Transaction注解表明支持事务等
基于约定的配置方式也常叫作“约定优于配置”或者“规约优于配置”Convention over Configuration。通过约定的代码结构或者命名来减少配置。说直白点就是提供配置的默认值优先使用默认值。程序员只需要设置那些偏离约定的配置就可以了。
比如在Spring JPA基于ORM框架、JPA规范的基础上封装的一套JPA应用框架我们约定类名默认跟表名相同属性名默认跟表字段名相同String类型对应数据库中的varchar类型long类型对应数据库中的bigint类型等等。
基于刚刚的约定代码中定义的Order类就对应数据库中的“order”表。只有在偏离这一约定的时候例如数据库中表命名为“order_info”而非“order”我们才需要显示地去配置类与表的映射关系Order类-&gt;order_info表
实际上约定优于配置很好地体现了“二八法则”。在平时的项目开发中80%的配置使用默认配置就可以了只有20%的配置必须用户显式地去设置。所以,基于约定来配置,在没有牺牲配置灵活性的前提下,节省了我们大量编写配置的时间,省掉了很多不动脑子的纯体力劳动,提高了开发效率。除此之外,基于相同的约定来做开发,也减少了项目的学习成本和维护成本。
### 2.低侵入、松耦合
框架的侵入性是衡量框架好坏的重要指标。所谓低侵入指的是,框架代码很少耦合在业务代码中。低侵入意味着,当我们要替换一个框架的时候,对原有的业务代码改动会很少。相反,如果一个框架是高度侵入的,代码高度侵入到业务代码中,那替换成另一个框架的成本将非常高,甚至几乎不可能。这也是一些长期维护的老项目,使用的框架、技术比较老旧,又无法更新的一个很重要的原因。
实际上低侵入是Spring框架遵循的一个非常重要的设计思想。
Spring提供的IOC容器在不需要Bean继承任何父类或者实现任何接口的情况下仅仅通过配置就能将它们纳入进Spring的管理中。如果我们换一个IOC容器也只是重新配置一下就可以了原有的Bean都不需要任何修改。
除此之外Spring提供的AOP功能也体现了低侵入的特性。在项目中对于非业务功能比如请求日志、数据采点、安全校验、事务等等我们没必要将它们侵入进业务代码中。因为一旦侵入这些代码将分散在各个业务代码中删除、修改的成本就变得很高。而基于AOP这种开发模式将非业务代码集中放到切面中删除、修改的成本就变得很低了。
### 3.模块化、轻量级
我们知道十几年前EJB是Java企业级应用的主流开发框架。但是它非常臃肿、复杂侵入性、耦合性高开发、维护和学习成本都不低。所以为了替代笨重的EJBRod Johnson开发了一套开源的Interface21框架提供了最基本的IOC功能。实际上Interface21框架就是Spring框架的前身。
但是随着不断的发展Spring现在也不单单只是一个只包含IOC功能的小框架了它显然已经壮大成了一个“平台”或者叫“生态”包含了各种五花八门的功能。尽管如此但它也并没有重蹈覆辙变成一个像EJB那样的庞大难用的框架。那Spring是怎么做到的呢
这就要归功于Spring的模块化设计思想。我们先看一张图如下所示它是Spring Framework的模块和分层介绍图。
<img src="https://static001.geekbang.org/resource/image/69/2c/699208dbe6b43ee397a020ea733c342c.png" alt="">
从图中我们可以看出Spring在分层、模块化方面做得非常好。每个模块都只负责一个相对独立的功能。模块之间关系仅有上层对下层的依赖关系而同层之间以及下层对上层几乎没有依赖和耦合。除此之外在依赖Spring的项目中开发者可以有选择地引入某几个模块而不会因为需要一个小的功能就被强迫引入整个Spring框架。所以尽管Spring Framework包含的模块很多已经有二十几个但每个模块都非常轻量级都可以单独拿来使用。正因如此到现在Spring框架仍然可以被称为是一个轻量级的开发框架。
### 4.再封装、再抽象
Spring不仅仅提供了各种Java项目开发的常用功能模块而且还对市面上主流的中间件、系统的访问类库做了进一步的封装和抽象提供了更高层次、更统一的访问接口。
比如Spring提供了spring-data-redis模块对Redis Java开发类库比如Jedis、Lettuce做了进一步的封装适配Spring的访问方式让编程访问Redis更加简单。
还有我们下节课要讲的Spring Cache实际上也是一种再封装、再抽象。它定义了统一、抽象的Cache访问接口这些接口不依赖具体的Cache实现Redis、Guava Cache、Caffeine等。在项目中我们基于Spring提供的抽象统一的接口来访问Cache。这样我们就能在不修改代码的情况下实现不同Cache之间的切换。
除此之外还记得我们之前在模板模式中讲过的JdbcTemplate吗实际上它也是对JDBC的进一步封装和抽象为的是进一步简化数据库编程。不仅如此Spring对JDBC异常也做了进一步的封装。封装的数据库异常继承自DataAccessException运行时异常。这类异常在开发中无需强制捕获从而减少了不必要的异常捕获和处理。除此之外Spring封装的数据库异常还屏蔽了不同数据库异常的细节比如不同的数据库对同一报错定义了不同的错误码让异常的处理更加简单。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
借助Spring框架我们总结了框架的作用解耦业务和非业务开发、让程序员聚焦在业务开发上隐藏复杂实现细节、降低开发难度、减少代码bug实现代码复用、节省开发时间规范化标准化项目开发、降低学习和维护成本等。实际上如果要用一句话来总结一下的话那就是简化开发
除此之外我们还重点讲解了Sping背后蕴含的一些经典设计思想主要有约定优于配置低侵入、松耦合模块化、轻量级再封装、再抽象。这些设计思想都比较通用我们可以借鉴到其他框架的开发中。
## 课堂讨论
1. “约定优于配置”在很多开发场景中都有体现比如Maven、Gradle构建工具它们约定了一套默认的项目目录结构除此之外你还能想到体现这条设计思想的其他哪些开发场景吗
1. 参照Spring的设计思想分析一个你熟悉框架、类库、功能组件背后的设计思想。
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,309 @@
<audio id="audio" title="85 | 开源实战四剖析Spring框架中用来支持扩展的两种设计模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/32/34/32ac984441874bd8bcd188af6f21d034.mp3"></audio>
上一节课中我们学习了Spring框架背后蕴藏的一些经典设计思想比如约定优于配置、低侵入松耦合、模块化轻量级等等。我们可以将这些设计思想借鉴到其他框架开发中在大的设计层面提高框架的代码质量。这也是我们在专栏中讲解这部分内容的原因。
除了上一节课中讲到的设计思想,实际上,可扩展也是大部分框架应该具备的一个重要特性。所谓的框架可扩展,我们之前也提到过,意思就是,框架使用者在不修改框架源码的情况下,基于扩展点定制扩展新的功能。
前面在理论部分我们也讲到常用来实现扩展特性的设计模式有观察者模式、模板模式、职责链模式、策略模式等。今天我们再剖析Spring框架为了支持可扩展特性用的2种设计模式观察者模式和模板模式。
话不多说,让我们正式开始今天的学习吧!
## 观察者模式在Spring中的应用
在前面我们讲到Java、Google Guava都提供了观察者模式的实现框架。Java提供的框架比较简单只包含java.util.Observable和java.util.Observer两个类。Google Guava提供的框架功能比较完善和强大通过EventBus事件总线来实现观察者模式。实际上Spring也提供了观察者模式的实现框架。今天我们就再来讲一讲它。
Spring中实现的观察者模式包含三部分Event事件相当于消息、Listener监听者相当于观察者、Publisher发送者相当于被观察者。我们通过一个例子来看下Spring提供的观察者模式是怎么使用的。代码如下所示
```
// Event事件
public class DemoEvent extends ApplicationEvent {
private String message;
public DemoEvent(Object source, String message) {
super(source);
}
public String getMessage() {
return this.message;
}
}
// Listener监听者
@Component
public class DemoListener implements ApplicationListener&lt;DemoEvent&gt; {
@Override
public void onApplicationEvent(DemoEvent demoEvent) {
String message = demoEvent.getMessage();
System.out.println(message);
}
}
// Publisher发送者
@Component
public class DemoPublisher {
@Autowired
private ApplicationContext applicationContext;
public void publishEvent(DemoEvent demoEvent) {
this.applicationContext.publishEvent(demoEvent);
}
}
```
从代码中我们可以看出框架使用起来并不复杂主要包含三部分工作定义一个继承ApplicationEvent的事件DemoEvent定义一个实现了ApplicationListener的监听器DemoListener定义一个发送者DemoPublisher发送者调用ApplicationContext来发送事件消息。
其中ApplicationEvent和ApplicationListener的代码实现都非常简单内部并不包含太多属性和方法。实际上它们最大的作用是做类型标识之用继承自ApplicationEvent的类是事件实现ApplicationListener的类是监听器
```
public abstract class ApplicationEvent extends EventObject {
private static final long serialVersionUID = 7099057708183571937L;
private final long timestamp = System.currentTimeMillis();
public ApplicationEvent(Object source) {
super(source);
}
public final long getTimestamp() {
return this.timestamp;
}
}
public class EventObject implements java.io.Serializable {
private static final long serialVersionUID = 5516075349620653480L;
protected transient Object source;
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException(&quot;null source&quot;);
this.source = source;
}
public Object getSource() {
return source;
}
public String toString() {
return getClass().getName() + &quot;[source=&quot; + source + &quot;]&quot;;
}
}
public interface ApplicationListener&lt;E extends ApplicationEvent&gt; extends EventListener {
void onApplicationEvent(E var1);
}
```
在前面讲到观察者模式的时候我们提到观察者需要事先注册到被观察者JDK的实现方式或者事件总线EventBus的实现方式中。那在Spring的实现中观察者注册到了哪里呢又是如何注册的呢
我想你应该猜到了我们把观察者注册到了ApplicationContext对象中。这里的ApplicationContext就相当于Google EventBus框架中的“事件总线”。不过稍微提醒一下ApplicationContext这个类并不只是为观察者模式服务的。它底层依赖BeanFactoryIOC的主要实现类提供应用启动、运行时的上下文信息是访问这些信息的最顶层接口。
实际上具体到源码来说ApplicationContext只是一个接口具体的代码实现包含在它的实现类AbstractApplicationContext中。我把跟观察者模式相关的代码摘抄到了下面。你只需要关注它是如何发送事件和注册监听者就好其他细节不需要细究。
```
public abstract class AbstractApplicationContext extends ... {
private final Set&lt;ApplicationListener&lt;?&gt;&gt; applicationListeners;
public AbstractApplicationContext() {
this.applicationListeners = new LinkedHashSet();
//...
}
public void publishEvent(ApplicationEvent event) {
this.publishEvent(event, (ResolvableType)null);
}
public void publishEvent(Object event) {
this.publishEvent(event, (ResolvableType)null);
}
protected void publishEvent(Object event, ResolvableType eventType) {
//...
Object applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent)event;
} else {
applicationEvent = new PayloadApplicationEvent(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
}
}
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
} else {
this.getApplicationEventMulticaster().multicastEvent(
(ApplicationEvent)applicationEvent, eventType);
}
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext)this.parent).publishEvent(event, eventType);
} else {
this.parent.publishEvent(event);
}
}
}
public void addApplicationListener(ApplicationListener&lt;?&gt; listener) {
Assert.notNull(listener, &quot;ApplicationListener must not be null&quot;);
if (this.applicationEventMulticaster != null) {
this.applicationEventMulticaster.addApplicationListener(listener);
} else {
this.applicationListeners.add(listener);
}
}
public Collection&lt;ApplicationListener&lt;?&gt;&gt; getApplicationListeners() {
return this.applicationListeners;
}
protected void registerListeners() {
Iterator var1 = this.getApplicationListeners().iterator();
while(var1.hasNext()) {
ApplicationListener&lt;?&gt; listener = (ApplicationListener)var1.next(); this.getApplicationEventMulticaster().addApplicationListener(listener);
}
String[] listenerBeanNames = this.getBeanNamesForType(ApplicationListener.class, true, false);
String[] var7 = listenerBeanNames;
int var3 = listenerBeanNames.length;
for(int var4 = 0; var4 &lt; var3; ++var4) {
String listenerBeanName = var7[var4];
this.getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
Set&lt;ApplicationEvent&gt; earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
Iterator var9 = earlyEventsToProcess.iterator();
while(var9.hasNext()) {
ApplicationEvent earlyEvent = (ApplicationEvent)var9.next();
this.getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
}
```
从上面的代码中我们发现真正的消息发送实际上是通过ApplicationEventMulticaster这个类来完成的。这个类的源码我只摘抄了最关键的一部分也就是multicastEvent()这个消息发送函数。不过,它的代码也并不复杂,我就不多解释了。这里我稍微提示一下,它通过线程池,支持异步非阻塞、同步阻塞这两种类型的观察者模式。
```
public void multicastEvent(ApplicationEvent event) {
this.multicastEvent(event, this.resolveDefaultEventType(event));
}
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
Iterator var4 = this.getApplicationListeners(event, type).iterator();
while(var4.hasNext()) {
final ApplicationListener&lt;?&gt; listener = (ApplicationListener)var4.next();
Executor executor = this.getTaskExecutor();
if (executor != null) {
executor.execute(new Runnable() {
public void run() {
SimpleApplicationEventMulticaster.this.invokeListener(listener, event);
}
});
} else {
this.invokeListener(listener, event);
}
}
}
```
借助Spring提供的观察者模式的骨架代码如果我们要在Spring下实现某个事件的发送和监听只需要做很少的工作定义事件、定义监听器、往ApplicationContext中发送事件就可以了剩下的工作都由Spring框架来完成。实际上这也体现了Spring框架的扩展性也就是在不需要修改任何代码的情况下扩展新的事件和监听。
## 模板模式在Spring中的应用
刚刚讲的是观察者模式在Spring中的应用现在我们再讲下模板模式。
我们来看下一下经常在面试中被问到的一个问题请你说下Spring Bean的创建过程包含哪些主要的步骤。这其中就涉及模板模式。它也体现了Spring的扩展性。利用模板模式Spring能让用户定制Bean的创建过程。
Spring Bean的创建过程可以大致分为两大步对象的创建和对象的初始化。
对象的创建是通过反射来动态生成对象而不是new方法。不管是哪种方式说白了总归还是调用构造函数来生成对象没有什么特殊的。对象的初始化有两种实现方式。一种是在类中自定义一个初始化函数并且通过配置文件显式地告知Spring哪个函数是初始化函数。我举了一个例子解释一下。如下所示在配置文件中我们通过init-method属性来指定初始化函数。
```
public class DemoClass {
//...
public void initDemo() {
//...初始化..
}
}
// 配置需要通过init-method显式地指定初始化方法
&lt;bean id=&quot;demoBean&quot; class=&quot;com.xzg.cd.DemoClass&quot; init-method=&quot;initDemo&quot;&gt;&lt;/bean&gt;
```
这种初始化方式有一个缺点初始化函数并不固定由用户随意定义这就需要Spring通过反射在运行时动态地调用这个初始化函数。而反射又会影响代码执行的性能那有没有替代方案呢
Spring提供了另外一个定义初始化函数的方法那就是让类实现Initializingbean接口。这个接口包含一个固定的初始化函数定义afterPropertiesSet()函数。Spring在初始化Bean的时候可以直接通过bean.afterPropertiesSet()的方式调用Bean对象上的这个函数而不需要使用反射来调用了。我举个例子解释一下代码如下所示。
```
public class DemoClass implements InitializingBean{
@Override
public void afterPropertiesSet() throws Exception {
//...初始化...
}
}
// 配置:不需要显式地指定初始化方法
&lt;bean id=&quot;demoBean&quot; class=&quot;com.xzg.cd.DemoClass&quot;&gt;&lt;/bean&gt;
```
尽管这种实现方式不会用到反射执行效率提高了但业务代码DemoClass跟框架代码InitializingBean耦合在了一起。框架代码侵入到了业务代码中替换框架的成本就变高了。所以我并不是太推荐这种写法。
实际上在Spring对Bean整个生命周期的管理中还有一个跟初始化相对应的过程那就是Bean的销毁过程。我们知道在Java中对象的回收是通过JVM来自动完成的。但是我们可以在将Bean正式交给JVM垃圾回收前执行一些销毁操作比如关闭文件句柄等等
销毁过程跟初始化过程非常相似也有两种实现方式。一种是通过配置destroy-method指定类中的销毁函数另一种是让类实现DisposableBean接口。因为destroy-method、DisposableBean跟init-method、InitializingBean非常相似所以这部分我们就不详细讲解了你可以自行研究下。
实际上Spring针对对象的初始化过程还做了进一步的细化将它拆分成了三个小步骤初始化前置操作、初始化、初始化后置操作。其中中间的初始化操作就是我们刚刚讲的那部分初始化的前置和后置操作定义在接口BeanPostProcessor中。BeanPostProcessor的接口定义如下所示
```
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object var1, String var2) throws BeansException;
Object postProcessAfterInitialization(Object var1, String var2) throws BeansException;
}
```
我们再来看下如何通过BeanPostProcessor来定义初始化前置和后置操作
我们只需要定义一个实现了BeanPostProcessor接口的处理器类并在配置文件中像配置普通Bean一样去配置就可以了。Spring中的ApplicationContext会自动检测在配置文件中实现了BeanPostProcessor接口的所有Bean并把它们注册到BeanPostProcessor处理器列表中。在Spring容器创建Bean的过程中Spring会逐一去调用这些处理器。
通过上面的分析我们基本上弄清楚了Spring Bean的整个生命周期创建加销毁。针对这个过程我画了一张图你可以结合着刚刚讲解一块看下。
<img src="https://static001.geekbang.org/resource/image/ca/4d/cacaf86b03a9432a4885385d2869264d.jpg" alt="">
不过,你可能会说,这里哪里用到了模板模式啊?模板模式不是需要定义一个包含模板方法的抽象模板类,以及定义子类实现模板方法吗?
实际上这里的模板模式的实现并不是标准的抽象类的实现方式而是有点类似我们前面讲到的Callback回调的实现方式也就是将要执行的函数封装成对象比如初始化方法封装成InitializingBean对象传递给模板BeanFactory来执行。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天我讲到了Spring中用到的两种支持扩展的设计模式观察者模式和模板模式。
其中观察者模式在Java、Google Guava、Spring中都有提供相应的实现代码。在平时的项目开发中基于这些实现代码我们可以轻松地实现一个观察者模式。
Java提供的框架比较简单只包含java.util.Observable和java.util.Observer两个类。Google Guava提供的框架功能比较完善和强大可以通过EventBus事件总线来实现观察者模式。Spring提供了观察者模式包含Event事件、Listener监听者、Publisher发送者三部分。事件发送到ApplicationContext中然后ApplicationConext将消息发送给事先注册好的监听者。
除此之外我们还讲到模板模式在Spring中的一个典型应用那就是Bean的创建过程。Bean的创建包含两个大的步骤对象的创建和对象的初始化。其中对象的初始化又可以分解为3个小的步骤初始化前置操作、初始化、初始化后置操作。
## 课堂讨论
在Google Guava的EventBus实现中被观察者发送消息到事件总线事件总线根据消息的类型将消息发送给可匹配的观察者。那在Spring提供的观察者模式的实现中是否也支持按照消息类型匹配观察者呢如果能它是如何实现的如果不能你有什么方法可以让它支持吗
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,485 @@
<audio id="audio" title="86 | 开源实战四总结Spring框架用到的11种设计模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a1/96/a1487f501e160e7dbc20de07c9b9cb96.mp3"></audio>
上一节课我们讲解了Spring中支持扩展功能的两种设计模式观察者模式和模板模式。这两种模式能够帮助我们创建扩展点让框架的使用者在不修改源码的情况下基于扩展点定制化框架功能。
实际上Spring框架中用到的设计模式非常多不下十几种。我们今天就总结罗列一下它们。限于篇幅我不可能对每种设计模式都进行非常详细的讲解。有些前面已经讲过的或者比较简单的我就点到为止。如果有什么不是很懂的地方你可以通过阅读源码查阅之前的理论讲解自己去搞定它。如果一直跟着我的课程学习相信你现在已经具备这样的学习能力。
话不多说,让我们正式开始今天的学习吧!
## 适配器模式在Spring中的应用
在Spring MVC中定义一个Controller最常用的方式是通过@Controller注解来标记某个类是Controller类,通过@RequesMapping注解来标记函数对应的URL。不过定义一个Controller远不止这一种方法。我们还可以通过让类实现Controller接口或者Servlet接口来定义一个Controller。针对这三种定义方式我写了三段示例代码如下所示
```
// 方法一:通过@Controller、@RequestMapping来定义
@Controller
public class DemoController {
@RequestMapping(&quot;/employname&quot;)
public ModelAndView getEmployeeName() {
ModelAndView model = new ModelAndView(&quot;Greeting&quot;);
model.addObject(&quot;message&quot;, &quot;Dinesh&quot;);
return model;
}
}
// 方法二实现Controller接口 + xml配置文件:配置DemoController与URL的对应关系
public class DemoController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
ModelAndView model = new ModelAndView(&quot;Greeting&quot;);
model.addObject(&quot;message&quot;, &quot;Dinesh Madhwal&quot;);
return model;
}
}
// 方法三实现Servlet接口 + xml配置文件:配置DemoController类与URL的对应关系
public class DemoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write(&quot;Hello World.&quot;);
}
}
```
在应用启动的时候Spring容器会加载这些Controller类并且解析出URL对应的处理函数封装成Handler对象存储到HandlerMapping对象中。当有请求到来的时候DispatcherServlet从HanderMapping中查找请求URL对应的Handler然后调用执行Handler对应的函数代码最后将执行结果返回给客户端。
但是不同方式定义的Controller其函数的定义函数名、入参、返回值等是不统一的。如上示例代码所示方法一中的函数的定义很随意、不固定方法二中的函数定义是handleRequest()、方法三中的函数定义是service()看似是定义了doGet()、doPost()实际上这里用到了模板模式Servlet中的service()调用了doGet()或doPost()方法DispatcherServlet调用的是service()方法。DispatcherServlet需要根据不同类型的Controller调用不同的函数。下面是具体的伪代码
```
Handler handler = handlerMapping.get(URL);
if (handler instanceof Controller) {
((Controller)handler).handleRequest(...);
} else if (handler instanceof Servlet) {
((Servlet)handler).service(...);
} else if (hanlder 对应通过注解来定义的Controller) {
反射调用方法...
}
```
从代码中我们可以看出这种实现方式会有很多if-else分支判断而且如果要增加一个新的Controller的定义方法我们就要在DispatcherServlet类代码中对应地增加一段如上伪代码所示的if逻辑。这显然不符合开闭原则。
实际上,我们可以利用是适配器模式对代码进行改造,让其满足开闭原则,能更好地支持扩展。在[第51节课](https://time.geekbang.org/column/article/205912)中我们讲到适配器其中一个作用是“统一多个类的接口设计”。利用适配器模式我们将不同方式定义的Controller类中的函数适配为统一的函数定义。这样我们就能在DispatcherServlet类代码中移除掉if-else分支判断逻辑调用统一的函数。
刚刚讲了大致的设计思路我们再具体看下Spring的代码实现。
Spring定义了统一的接口HandlerAdapter并且对每种Controller定义了对应的适配器类。这些适配器类包括AnnotationMethodHandlerAdapter、SimpleControllerHandlerAdapter、SimpleServletHandlerAdapter等。源码我贴到了下面你可以结合着看下。
```
public interface HandlerAdapter {
boolean supports(Object var1);
ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;
long getLastModified(HttpServletRequest var1, Object var2);
}
// 对应实现Controller接口的Controller
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
public SimpleControllerHandlerAdapter() {
}
public boolean supports(Object handler) {
return handler instanceof Controller;
}
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return ((Controller)handler).handleRequest(request, response);
}
public long getLastModified(HttpServletRequest request, Object handler) {
return handler instanceof LastModified ? ((LastModified)handler).getLastModified(request) : -1L;
}
}
// 对应实现Servlet接口的Controller
public class SimpleServletHandlerAdapter implements HandlerAdapter {
public SimpleServletHandlerAdapter() {
}
public boolean supports(Object handler) {
return handler instanceof Servlet;
}
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
((Servlet)handler).service(request, response);
return null;
}
public long getLastModified(HttpServletRequest request, Object handler) {
return -1L;
}
}
//AnnotationMethodHandlerAdapter对应通过注解实现的Controller
//代码太多了,我就不贴在这里了
```
在DispatcherServlet类中我们就不需要区分对待不同的Controller对象了统一调用HandlerAdapter的handle()函数就可以了。按照这个思路实现的伪代码如下所示。你看这样就没有烦人的if-else逻辑了吧
```
// 之前的实现方式
Handler handler = handlerMapping.get(URL);
if (handler instanceof Controller) {
((Controller)handler).handleRequest(...);
} else if (handler instanceof Servlet) {
((Servlet)handler).service(...);
} else if (hanlder 对应通过注解来定义的Controller) {
反射调用方法...
}
// 现在实现方式
HandlerAdapter handlerAdapter = handlerMapping.get(URL);
handlerAdapter.handle(...);
```
## 策略模式在Spring中的应用
我们前面讲到Spring AOP是通过动态代理来实现的。熟悉Java的同学应该知道具体到代码实现Spring支持两种动态代理实现方式一种是JDK提供的动态代理实现方式另一种是Cglib提供的动态代理实现方式。
前者需要被代理的类有抽象的接口定义后者不需要这两种动态代理实现方式的更多区别请自行百度研究吧。针对不同的被代理类Spring会在运行时动态地选择不同的动态代理实现方式。这个应用场景实际上就是策略模式的典型应用场景。
我们前面讲过策略模式包含三部分策略的定义、创建和使用。接下来我们具体看下这三个部分是如何体现在Spring源码中的。
在策略模式中策略的定义这一部分很简单。我们只需要定义一个策略接口让不同的策略类都实现这一个策略接口。对应到Spring源码AopProxy是策略接口JdkDynamicAopProxy、CglibAopProxy是两个实现了AopProxy接口的策略类。其中AopProxy接口的定义如下所示
```
public interface AopProxy {
Object getProxy();
Object getProxy(ClassLoader var1);
}
```
在策略模式中策略的创建一般通过工厂方法来实现。对应到Spring源码AopProxyFactory是一个工厂类接口DefaultAopProxyFactory是一个默认的工厂类用来创建AopProxy对象。两者的源码如下所示
```
public interface AopProxyFactory {
AopProxy createAopProxy(AdvisedSupport var1) throws AopConfigException;
}
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
public DefaultAopProxyFactory() {
}
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (!config.isOptimize() &amp;&amp; !config.isProxyTargetClass() &amp;&amp; !this.hasNoUserSuppliedProxyInterfaces(config)) {
return new JdkDynamicAopProxy(config);
} else {
Class&lt;?&gt; targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException(&quot;TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.&quot;);
} else {
return (AopProxy)(!targetClass.isInterface() &amp;&amp; !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
}
}
}
//用来判断用哪个动态代理实现方式
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class&lt;?&gt;[] ifcs = config.getProxiedInterfaces();
return ifcs.length == 0 || ifcs.length == 1 &amp;&amp; SpringProxy.class.isAssignableFrom(ifcs[0]);
}
}
```
策略模式的典型应用场景一般是通过环境变量、状态值、计算结果等动态地决定使用哪个策略。对应到Spring源码中我们可以参看刚刚给出的DefaultAopProxyFactory类中的createAopProxy()函数的代码实现。其中第10行代码是动态选择哪种策略的判断条件。
## 组合模式在Spring中的应用
上节课讲到Spring“再封装、再抽象”设计思想的时候我们提到了Spring Cache。Spring Cache提供了一套抽象的Cache接口。使用它我们能够统一不同缓存实现Redis、Google Guava…的不同的访问方式。Spring中针对不同缓存实现的不同缓存访问类都依赖这个接口比如EhCacheCache、GuavaCache、NoOpCache、RedisCache、JCacheCache、ConcurrentMapCache、CaffeineCache。Cache接口的源码如下所示
```
public interface Cache {
String getName();
Object getNativeCache();
Cache.ValueWrapper get(Object var1);
&lt;T&gt; T get(Object var1, Class&lt;T&gt; var2);
&lt;T&gt; T get(Object var1, Callable&lt;T&gt; var2);
void put(Object var1, Object var2);
Cache.ValueWrapper putIfAbsent(Object var1, Object var2);
void evict(Object var1);
void clear();
public static class ValueRetrievalException extends RuntimeException {
private final Object key;
public ValueRetrievalException(Object key, Callable&lt;?&gt; loader, Throwable ex) {
super(String.format(&quot;Value for key '%s' could not be loaded using '%s'&quot;, key, loader), ex);
this.key = key;
}
public Object getKey() {
return this.key;
}
}
public interface ValueWrapper {
Object get();
}
}
```
在实际的开发中一个项目有可能会用到多种不同的缓存比如既用到Google Guava缓存也用到Redis缓存。除此之外同一个缓存实例也可以根据业务的不同分割成多个小的逻辑缓存单元或者叫作命名空间
为了管理多个缓存Spring还提供了缓存管理功能。不过它包含的功能很简单主要有这样两部分一个是根据缓存名字创建Cache对象的时候要设置name属性获取Cache对象另一个是获取管理器管理的所有缓存的名字列表。对应的Spring源码如下所示
```
public interface CacheManager {
Cache getCache(String var1);
Collection&lt;String&gt; getCacheNames();
}
```
刚刚给出的是CacheManager接口的定义那如何来实现这两个接口呢实际上这就要用到了我们之前讲过的组合模式。
我们前面讲过组合模式主要应用在能表示成树形结构的一组数据上。树中的结点分为叶子节点和中间节点两类。对应到Spring源码EhCacheManager、SimpleCacheManager、NoOpCacheManager、RedisCacheManager等表示叶子节点CompositeCacheManager表示中间节点。
叶子节点包含的是它所管理的Cache对象中间节点包含的是其他CacheManager管理器既可以是CompositeCacheManager也可以是具体的管理器比如EhCacheManager、RedisManager等。
我把CompositeCacheManger的代码贴到了下面你可以结合着讲解一块看下。其中getCache()、getCacheNames()两个函数的实现都用到了递归。这正是树形结构最能发挥优势的地方。
```
public class CompositeCacheManager implements CacheManager, InitializingBean {
private final List&lt;CacheManager&gt; cacheManagers = new ArrayList();
private boolean fallbackToNoOpCache = false;
public CompositeCacheManager() {
}
public CompositeCacheManager(CacheManager... cacheManagers) {
this.setCacheManagers(Arrays.asList(cacheManagers));
}
public void setCacheManagers(Collection&lt;CacheManager&gt; cacheManagers) {
this.cacheManagers.addAll(cacheManagers);
}
public void setFallbackToNoOpCache(boolean fallbackToNoOpCache) {
this.fallbackToNoOpCache = fallbackToNoOpCache;
}
public void afterPropertiesSet() {
if (this.fallbackToNoOpCache) {
this.cacheManagers.add(new NoOpCacheManager());
}
}
public Cache getCache(String name) {
Iterator var2 = this.cacheManagers.iterator();
Cache cache;
do {
if (!var2.hasNext()) {
return null;
}
CacheManager cacheManager = (CacheManager)var2.next();
cache = cacheManager.getCache(name);
} while(cache == null);
return cache;
}
public Collection&lt;String&gt; getCacheNames() {
Set&lt;String&gt; names = new LinkedHashSet();
Iterator var2 = this.cacheManagers.iterator();
while(var2.hasNext()) {
CacheManager manager = (CacheManager)var2.next();
names.addAll(manager.getCacheNames());
}
return Collections.unmodifiableSet(names);
}
}
```
## 装饰器模式在Spring中的应用
我们知道,缓存一般都是配合数据库来使用的。如果写缓存成功,但数据库事务回滚了,那缓存中就会有脏数据。为了解决这个问题,我们需要将缓存的写操作和数据库的写操作,放到同一个事务中,要么都成功,要么都失败。
实现这样一个功能Spring使用到了装饰器模式。TransactionAwareCacheDecorator增加了对事务的支持在事务提交、回滚的时候分别对Cache的数据进行处理。
TransactionAwareCacheDecorator实现Cache接口并且将所有的操作都委托给targetCache来实现对其中的写操作添加了事务功能。这是典型的装饰器模式的应用场景和代码实现我就不多作解释了。
```
public class TransactionAwareCacheDecorator implements Cache {
private final Cache targetCache;
public TransactionAwareCacheDecorator(Cache targetCache) {
Assert.notNull(targetCache, &quot;Target Cache must not be null&quot;);
this.targetCache = targetCache;
}
public Cache getTargetCache() {
return this.targetCache;
}
public String getName() {
return this.targetCache.getName();
}
public Object getNativeCache() {
return this.targetCache.getNativeCache();
}
public ValueWrapper get(Object key) {
return this.targetCache.get(key);
}
public &lt;T&gt; T get(Object key, Class&lt;T&gt; type) {
return this.targetCache.get(key, type);
}
public &lt;T&gt; T get(Object key, Callable&lt;T&gt; valueLoader) {
return this.targetCache.get(key, valueLoader);
}
public void put(final Object key, final Object value) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.put(key, value);
}
});
} else {
this.targetCache.put(key, value);
}
}
public ValueWrapper putIfAbsent(Object key, Object value) {
return this.targetCache.putIfAbsent(key, value);
}
public void evict(final Object key) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.evict(key);
}
});
} else {
this.targetCache.evict(key);
}
}
public void clear() {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.clear();
}
});
} else {
this.targetCache.clear();
}
}
}
```
## 工厂模式在Spring中的应用
在Spring中工厂模式最经典的应用莫过于实现IOC容器对应的Spring源码主要是BeanFactory类和ApplicationContext相关类AbstractApplicationContext、ClassPathXmlApplicationContext、FileSystemXmlApplicationContext…。除此之外在理论部分我还带你手把手实现了一个简单的IOC容器。你可以回过头去再看下。
在Spring中创建Bean的方式有很多种比如前面提到的纯构造函数、无参构造函数加setter方法。我写了一个例子来说明这两种创建方式代码如下所示
```
public class Student {
private long id;
private String name;
public Student(long id, String name) {
this.id = id;
this.name = name;
}
public void setId(long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}
// 使用构造函数来创建Bean
&lt;bean id=&quot;student&quot; class=&quot;com.xzg.cd.Student&quot;&gt;
&lt;constructor-arg name=&quot;id&quot; value=&quot;1&quot;/&gt;
&lt;constructor-arg name=&quot;name&quot; value=&quot;wangzheng&quot;/&gt;
&lt;/bean&gt;
// 使用无参构造函数+setter方法来创建Bean
&lt;bean id=&quot;student&quot; class=&quot;com.xzg.cd.Student&quot;&gt;
&lt;property name=&quot;id&quot; value=&quot;1&quot;&gt;&lt;/property&gt;
&lt;property name=&quot;name&quot; value=&quot;wangzheng&quot;&gt;&lt;/property&gt;
&lt;/bean&gt;
```
实际上除了这两种创建Bean的方式之外我们还可以通过工厂方法来创建Bean。还是刚刚这个例子用这种方式来创建Bean的话就是下面这个样子
```
public class StudentFactory {
private static Map&lt;Long, Student&gt; students = new HashMap&lt;&gt;();
static{
map.put(1, new Student(1,&quot;wang&quot;));
map.put(2, new Student(2,&quot;zheng&quot;));
map.put(3, new Student(3,&quot;xzg&quot;));
}
public static Student getStudent(long id){
return students.get(id);
}
}
// 通过工厂方法getStudent(2)来创建BeanId=&quot;zheng&quot;&quot;的Bean
&lt;bean id=&quot;zheng&quot; class=&quot;com.xzg.cd.StudentFactory&quot; factory-method=&quot;getStudent&quot;&gt;
&lt;constructor-arg value=&quot;2&quot;&gt;&lt;/constructor-arg&gt;
&lt;/bean&gt;
```
## 其他模式在Spring中的应用
前面的几个模式在Spring中的应用讲解的都比较详细接下来的几个模式大部分都是我们之前讲过的这里只是简单总结一下点到为止如果你对哪块有遗忘可以回过头去看下理论部分的讲解。
SpEL全称叫Spring Expression Language是Spring中常用来编写配置的表达式语言。它定义了一系列的语法规则。我们只要按照这些语法规则来编写表达式Spring就能解析出表达式的含义。实际上这就是我们前面讲到的解释器模式的典型应用场景。
因为解释器模式没有一个非常固定的代码实现结构而且Spring中SpEL相关的代码也比较多所以这里就不带你一块阅读源码了。如果感兴趣或者项目中正好要实现类似的功能的时候你可以再去阅读、借鉴它的代码实现。代码主要集中在spring-expresssion这个模块下面。
前面讲到单例模式的时候我提到过单例模式有很多弊端比如单元测试不友好等。应对策略就是通过IOC容器来管理对象通过IOC容器来实现对象的唯一性的控制。实际上这样实现的单例并非真正的单例它的唯一性的作用范围仅仅在同一个IOC容器内。
除此之外Spring还用到了观察者模式、模板模式、职责链模式、代理模式。其中观察者模式、模板模式在上一节课已经详细讲过了。
实际上在Spring中只要后缀带有Template的类基本上都是模板类而且大部分都是用Callback回调来实现的比如JdbcTemplate、RedisTemplate等。剩下的两个模式在Spring中的应用应该人尽皆知了。职责链模式在Spring中的应用是拦截器Interceptor代理模式经典应用是AOP。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
我们今天提到的设计模式有11种它们分别是适配器模式、策略模式、组合模式、装饰器模式、工厂模式、单例模式、解释器模式、观察者模式、模板模式、职责链模式、代理模式基本上占了23种设计模式的一半。这还只是我所知道的实际上Spring用到的设计模式可能还要更多。你看设计模式并非“花拳绣腿”吧它在实际的项目开发中确实有很多应用确实可以发挥很大的作用。
还是那句话对于今天的内容你不需要去记忆哪个类用到了哪个设计模式。你只需要跟着我的讲解把每个设计模式在Spring中的应用场景搞懂就可以了。看到类似的代码能够立马识别出它用到了哪种设计模式看到类似的应用场景能够立马反映出要用哪种模式去解决这样就说明你已经掌握得足够好了。
## 课堂讨论
我们前面讲到除了纯构造函数、构造函数加setter方法和工厂方法之外还有另外一个经常用来创建对象的模式Builder模式。如果我们让Spring支持通过Builder模式来创建Bean应该如何来编写代码和配置呢你可以设计一下吗
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,125 @@
<audio id="audio" title="87 | 开源实战五MyBatis如何权衡易用性、性能和灵活性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fe/11/fe2ff9b2f415fbf49c10e74f71aea411.mp3"></audio>
上几节课我们讲到了Spring框架剖析了背后蕴含的一些通用设计思想以及用到的十几种设计模式。从今天开始我们再剖析另外一个Java项目开发中经常用到的框架MyBatis。因为内容比较多同样我们也分三节课来讲解。
- 第一节课我们分析MyBatis如何权衡代码的易用性、性能和灵活性。
- 第二节课我们学习如何利用职责链与代理模式实现MyBatis Plugin。
- 第三节课我们总结罗列一下MyBatis框架中用到的十几种设计模式。
话不多说,让我们正式开始今天的学习吧!
## Mybatis和ORM框架介绍
熟悉Java的同学应该知道MyBatis是一个ORMObject Relational Mapping对象-关系映射框架。ORM框架主要是根据类和数据库表之间的映射关系帮助程序员自动实现对象与数据库中数据之间的互相转化。说得更具体点就是ORM负责将程序中的对象存储到数据库中、将数据库中的数据转化为程序中的对象。实际上Java中的ORM框架有很多除了刚刚提到的MyBatis之外还有Hibernate、TopLink等。
在剖析Spring框架的时候我们讲到如果用一句话来总结框架作用的话那就是简化开发。MyBatis框架也不例外。它简化的是数据库方面的开发。那MyBatis是如何简化数据库开发的呢我们结合[第59讲](https://time.geekbang.org/column/article/212802)中的JdbcTemplate的例子来说明一下。
在第59讲中我们讲到Java提供了JDBC类库来封装不同类型的数据库操作。不过直接使用JDBC来进行数据库编程还是有点麻烦的。于是Spring提供了JdbcTemplate对JDBC进一步封装来进一步简化数据库编程。
使用JdbcTemplate进行数据库编程我们只需要编写跟业务相关的代码比如SQL语句、数据库中数据与对象之间的互相转化的代码其他流程性质的代码比如加载驱动、创建数据库连接、创建statement、关闭连接、关闭statement等都封装在了JdbcTemplate类中不需要我们重复编写。
当时为了展示使用JdbcTemplate是如何简化数据库编程的我们还举了一个查询数据库中用户信息的例子。还是同样这个例子我再来看下使用MyBatis该如何实现是不是比使用JdbcTemplate更加简单。
因为MyBatis依赖JDBC驱动所以在项目中使用MyBatis除了需要引入MyBatis框架本身mybatis.jar之外还需要引入JDBC驱动比如访问MySQL的JDBC驱动实现类库mysql-connector-java.jar。将两个jar包引入项目之后我们就可以开始编程了。使用MyBatis来访问数据库中用户信息的代码如下所示
```
// 1. 定义UserDO
public class UserDo {
private long id;
private String name;
private String telephone;
// 省略setter/getter方法
}
// 2. 定义访问接口
public interface UserMapper {
public UserDo selectById(long id);
}
// 3. 定义映射关系UserMapper.xml
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org/DTD Mapper 3.0//EN&quot;
&quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot; &gt;
&lt;mapper namespace=&quot;cn.xzg.cd.a87.repo.mapper.UserMapper&quot;&gt;
&lt;select id=&quot;selectById&quot; resultType=&quot;cn.xzg.cd.a87.repo.UserDo&quot;&gt;
select * from user where id=#{id}
&lt;/select&gt;
&lt;/mapper&gt;
// 4. 全局配置文件: mybatis.xml
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&lt;!DOCTYPE configuration
PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;
&quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&gt;
&lt;configuration&gt;
&lt;environments default=&quot;dev&quot;&gt;
&lt;environment id=&quot;dev&quot;&gt;
&lt;transactionManager type=&quot;JDBC&quot;&gt;&lt;/transactionManager&gt;
&lt;dataSource type=&quot;POOLED&quot;&gt;
&lt;property name=&quot;driver&quot; value=&quot;com.mysql.jdbc.Driver&quot; /&gt;
&lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8&quot; /&gt;
&lt;property name=&quot;username&quot; value=&quot;root&quot; /&gt;
&lt;property name=&quot;password&quot; value=&quot;...&quot; /&gt;
&lt;/dataSource&gt;
&lt;/environment&gt;
&lt;/environments&gt;
&lt;mappers&gt;
&lt;mapper resource=&quot;mapper/UserMapper.xml&quot;/&gt;
&lt;/mappers&gt;
&lt;/configuration&gt;
```
需要注意的是在UserMapper.xml配置文件中我们只定义了接口和SQL语句之间的映射关系并没有显式地定义类UserDo字段与数据库表user字段之间的映射关系。实际上这就体现了“约定优于配置”的设计原则。类字段与数据库表字段之间使用了默认映射关系类字段跟数据库表中拼写相同的字段一一映射。当然如果没办法做到一一映射我们也可以自定义它们之间的映射关系。
有了上面的代码和配置,我们就可以像下面这样来访问数据库中的用户信息了。
```
public class MyBatisDemo {
public static void main(String[] args) throws IOException {
Reader reader = Resources.getResourceAsReader(&quot;mybatis.xml&quot;);
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sessionFactory.openSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
UserDo userDo = userMapper.selectById(8);
//...
}
}
```
从代码中我们可以看出相对于使用JdbcTemplate的实现方式使用MyBatis的实现方式更加灵活。在使用JdbcTemplate的实现方式中对象与数据库中数据之间的转化代码、SQL语句是硬编码在业务代码中的。而在使用MyBatis的实现方式中类字段与数据库字段之间的映射关系、接口与SQL之间的映射关系是写在XML配置文件中的是跟代码相分离的这样会更加灵活、清晰维护起来更加方便。
## 如何平衡易用性、性能和灵活性?
刚刚我们对MyBatis框架做了简单介绍接下来我们再对比一下另外两个框架JdbcTemplate和Hibernate。通过对比我们来看MyBatis是如何权衡代码的易用性、性能和灵活性的。
我们先来看JdbcTemplate。相对于MyBatis来说JdbcTemplate更加轻量级。因为它对JDBC只做了很简单的封装所以性能损耗比较少。相对于其他两个框架来说它的性能最好。但是它的缺点也比较明显那就是SQL与代码耦合在一起而且不具备ORM的功能需要自己编写代码解析对象跟数据库中的数据之间的映射关系。所以在易用性上它不及其他两个框架。
我们再来看Hibernate。相对于MyBatis来说Hibernate更加重量级。Hibernate提供了更加高级的映射功能能够根据业务需求自动生成SQL语句。我们不需要像使用MyBatis那样自己编写SQL。因此有的时候我们也把MyBatis称作半自动化的ORM框架把Hibernate称作全自动化的ORM框架。不过虽然自动生成SQL简化了开发但是毕竟是自动生成的没有针对性的优化。在性能方面这样得到的SQL可能没有程序员编写得好。同时这样也丧失了程序员自己编写SQL的灵活性。
实际上,不管用哪种实现方式,从数据库中取出数据并且转化成对象,这个过程涉及的代码逻辑基本是一致的。不同实现方式的区别,只不过是哪部分代码逻辑放到了哪里。有的框架提供的功能比较强大,大部分代码逻辑都由框架来完成,程序员只需要实现很小的一部分代码就可以了。这样框架的易用性就更好些。但是,框架集成的功能越多,为了处理逻辑的通用性,就会引入更多额外的处理代码。比起针对具体问题具体编程,这样性能损耗就相对大一些。
所以,粗略地讲,有的时候,框架的易用性和性能成对立关系。追求易用性,那性能就差一些。相反,追求性能,易用性就差一些。除此之外,使用起来越简单,那灵活性就越差。这就好比我们用的照相机。傻瓜相机按下快门就能拍照,但没有复杂的单反灵活。
实际上JdbcTemplate、MyBatis、Hibernate这几个框架也体现了刚刚说的这个规律。
JdbcTemplate提供的功能最简单易用性最差性能损耗最少用它编程性能最好。Hibernate提供的功能最完善易用性最好但相对来说性能损耗就最高了。MyBatis介于两者中间在易用性、性能、灵活性三个方面做到了权衡。它支撑程序员自己编写SQL能够延续程序员对SQL知识的积累。相对于完全黑盒子的Hibernate很多程序员反倒是更加喜欢MyBatis这种半透明的框架。这也提醒我们过度封装提供过于简化的开发方式也会丧失开发的灵活性。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
如果你熟悉Java和MyBatis那你应该掌握今天讲到JDBC、JdbcTemplate、MyBatis、Hibernate之间的区别。JDBC是Java访问数据库的开发规范提供了一套抽象的统一的开发接口隐藏不同数据库的访问细节。
JdbcTemplate、MyBatis、Hibernate都是对JDBC的二次封装为的是进一步简化数据库开发。其中JdbcTemplate不能算得上是ORM框架因为还需要程序员自己编程来实现对象和数据库数据之间的互相转化。相对于Hibernate这种连SQL都不用程序员自己写的全自动ORM框架MyBatis算是一种半自动化的ORM框架。
如果你不熟悉Java和MyBatis作为背景介绍那你简单了解一下MyBatis和ORM就可以了。不过在你熟悉的语言中应该也有相应的ORM框架你也可以对比着去分析一下。
今天的内容除了起到对MyBatis做背景介绍之外我们还学习了代码的易用性、性能、灵活性之间的关系。一般来讲提供的高级功能越多那性能损耗就会越大些用起来越简单提供越简化的开发方式那灵活性也就相对越低。
## 课堂讨论
在你的项目开发中,有没有用过哪些框架,能够切实地提高开发效率,减少不必要的体力劳动?
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,330 @@
<audio id="audio" title="88 | 开源实战五如何利用职责链与代理模式实现MyBatis Plugin" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6a/05/6a68abdb299dc47e6fa0a94575638e05.mp3"></audio>
上节课我们对MyBatis框架做了简单的背景介绍并且通过对比各种ORM框架学习了代码的易用性、性能、灵活性之间的关系。一般来讲框架提供的高级功能越多那性能损耗就会越大框架用起来越简单提供越简化的使用方式那灵活性也就越低。
接下来的两节课我们再学习一下MyBatis用到一些经典设计模式。其中今天我们主要讲解MyBatis Plugin。尽管名字叫Plugin插件但它实际上跟之前讲到的Servlet Filter过滤器、Spring Interceptor拦截器类似设计的初衷都是为了框架的扩展性用到的主要设计模式都是职责链模式。
不过相对于Servlet Filter和Spring InterceptorMyBatis Plugin中职责链模式的代码实现稍微有点复杂。它是借助动态代理模式来实现的职责链。今天我就带你看下如何利用这两个模式实现MyBatis Plugin。
话不多说,让我们正式开始今天的学习吧!
## MyBatis Plugin功能介绍
实际上MyBatis Plugin跟Servlet Filter、Spring Interceptor的功能是类似的都是在不需要修改原有流程代码的情况下拦截某些方法调用在拦截的方法调用的前后执行一些额外的代码逻辑。它们的唯一区别在于拦截的位置是不同的。Servlet Filter主要拦截Servlet请求Spring Interceptor主要拦截Spring管理的Bean方法比如Controller类的方法等而MyBatis Plugin主要拦截的是MyBatis在执行SQL的过程中涉及的一些方法。
MyBatis Plugin使用起来比较简单我们通过一个例子来快速看下。
假设我们需要统计应用中每个SQL的执行耗时如果使用MyBatis Plugin来实现的话我们只需要定义一个SqlCostTimeInterceptor类让它实现MyBatis的Interceptor接口并且在MyBatis的全局配置文件中简单声明一下这个插件就可以了。具体的代码和配置如下所示
```
@Intercepts({
@Signature(type = StatementHandler.class, method = &quot;query&quot;, args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = &quot;update&quot;, args = {Statement.class}),
@Signature(type = StatementHandler.class, method = &quot;batch&quot;, args = {Statement.class})})
public class SqlCostTimeInterceptor implements Interceptor {
private static Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
long startTime = System.currentTimeMillis();
StatementHandler statementHandler = (StatementHandler) target;
try {
return invocation.proceed();
} finally {
long costTime = System.currentTimeMillis() - startTime;
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
logger.info(&quot;执行 SQL[ {} ]执行耗时[ {} ms]&quot;, sql, costTime);
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
System.out.println(&quot;插件配置的信息:&quot;+properties);
}
}
&lt;!-- MyBatis全局配置文件mybatis-config.xml --&gt;
&lt;plugins&gt;
&lt;plugin interceptor=&quot;com.xzg.cd.a88.SqlCostTimeInterceptor&quot;&gt;
&lt;property name=&quot;someProperty&quot; value=&quot;100&quot;/&gt;
&lt;/plugin&gt;
&lt;/plugins&gt;
```
因为待会我会详细地介绍MyBatis Plugin的底层实现原理所以这里暂时不对上面的代码做详细地解释。现在我们只重点看下@Intercepts注解这一部分
我们知道,不管是拦截器、过滤器还是插件,都需要明确地标明拦截的目标方法。@Intercepts注解实际上就是起了这个作用。其中,@Intercepts注解又可以嵌套@Signature注解。一个@Signature注解标明一个要拦截的目标方法。如果要拦截多个方法,我们可以像例子中那样,编写多条@Signature注解
@Signature注解包含三个元素type、method、args。其中type指明要拦截的类、method指明方法名、args指明方法的参数列表。通过指定这三个元素我们就能完全确定一个要拦截的方法。
默认情况下MyBatis Plugin允许拦截的方法有下面这样几个
<img src="https://static001.geekbang.org/resource/image/cd/d1/cd0aae4a0758ac0913ad28988a6718d1.jpg" alt="">
为什么默认允许拦截的是这样几个类的方法呢?
MyBatis底层是通过Executor类来执行SQL的。Executor类会创建StatementHandler、ParameterHandler、ResultSetHandler三个对象并且首先使用ParameterHandler设置SQL中的占位符参数然后使用StatementHandler执行SQL语句最后使用ResultSetHandler封装执行结果。所以我们只需要拦截Executor、ParameterHandler、ResultSetHandler、StatementHandler这几个类的方法基本上就能满足我们对整个SQL执行流程的拦截了。
实际上除了统计SQL的执行耗时利用MyBatis Plugin我们还可以做很多事情比如分库分表、自动分页、数据脱敏、加密解密等等。如果感兴趣的话你可以自己实现一下。
## MyBatis Plugin的设计与实现
刚刚我们简单介绍了MyBatis Plugin是如何使用的。现在我们再剖析一下源码看看如此简洁的使用方式底层是如何实现的隐藏了哪些复杂的设计。
相对于Servlet Filter、Spring Interceptor中职责链模式的代码实现MyBatis Plugin的代码实现还是蛮有技巧的因为它是借助动态代理来实现职责链的。
在[第62节](https://time.geekbang.org/column/article/216278)和[第63节](https://time.geekbang.org/column/article/217395)中我们讲到职责链模式的实现一般包含处理器Handler和处理器链HandlerChain两部分。这两个部分对应到Servlet Filter的源码就是Filter和FilterChain对应到Spring Interceptor的源码就是HandlerInterceptor和HandlerExecutionChain对应到MyBatis Plugin的源码就是Interceptor和InterceptorChain。除此之外MyBatis Plugin还包含另外一个非常重要的类Plugin。它用来生成被拦截对象的动态代理。
集成了MyBatis的应用在启动的时候MyBatis框架会读取全局配置文件前面例子中的mybatis-config.xml文件解析出Interceptor也就是例子中的SqlCostTimeInterceptor并且将它注入到Configuration类的InterceptorChain对象中。这部分逻辑对应到源码如下所示
```
public class XMLConfigBuilder extends BaseBuilder {
//解析配置
private void parseConfiguration(XNode root) {
try {
//省略部分代码...
pluginElement(root.evalNode(&quot;plugins&quot;)); //解析插件
} catch (Exception e) {
throw new BuilderException(&quot;Error parsing SQL Mapper Configuration. Cause: &quot; + e, e);
}
}
//解析插件
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute(&quot;interceptor&quot;);
Properties properties = child.getChildrenAsProperties();
//创建Interceptor类对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//调用Interceptor上的setProperties()方法设置properties
interceptorInstance.setProperties(properties);
//下面这行代码会调用InterceptorChain.addInterceptor()方法
configuration.addInterceptor(interceptorInstance);
}
}
}
}
// Configuration类的addInterceptor()方法的代码如下所示
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
```
我们再来看Interceptor和InterceptorChain这两个类的代码如下所示。Interceptor的setProperties()方法就是一个单纯的setter方法主要是为了方便通过配置文件配置Interceptor的一些属性值没有其他作用。Interceptor类中intecept()和plugin()函数以及InterceptorChain类中的pluginAll()函数,是最核心的三个函数,我们待会再详细解释。
```
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
// 省略构造函数和getter方法...
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
public class InterceptorChain {
private final List&lt;Interceptor&gt; interceptors = new ArrayList&lt;Interceptor&gt;();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List&lt;Interceptor&gt; getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
```
解析完配置文件之后所有的Interceptor都加载到了InterceptorChain中。接下来我们再来看下这些拦截器是在什么时候被触发执行的又是如何被触发执行的呢
前面我们提到在执行SQL的过程中MyBatis会创建Executor、StatementHandler、ParameterHandler、ResultSetHandler这几个类的对象对应的创建代码在Configuration类中如下所示
```
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
```
从上面的代码中我们可以发现这几个类对象的创建过程都调用了InteceptorChain的pluginAll()方法。这个方法的代码前面已经给出了。你可以回过头去再看一眼。它的代码实现很简单嵌套调用InterceptorChain上每个Interceptor的plugin()方法。plugin()是一个接口方法不包含实现代码需要由用户给出具体的实现代码。在之前的例子中SQLTimeCostInterceptor的plugin()方法通过直接调用Plugin的wrap()方法来实现。wrap()方法的代码实现如下所示:
```
// 借助Java InvocationHandler实现的动态代理模式
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map&lt;Class&lt;?&gt;, Set&lt;Method&gt;&gt; signatureMap;
private Plugin(Object target, Interceptor interceptor, Map&lt;Class&lt;?&gt;, Set&lt;Method&gt;&gt; signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
// wrap()静态方法用来生成target的动态代理
// 动态代理对象=target对象+interceptor对象。
public static Object wrap(Object target, Interceptor interceptor) {
Map&lt;Class&lt;?&gt;, Set&lt;Method&gt;&gt; signatureMap = getSignatureMap(interceptor);
Class&lt;?&gt; type = target.getClass();
Class&lt;?&gt;[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length &gt; 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
// 调用target上的f()方法,会触发执行下面这个方法。
// 这个方法包含执行interceptor的intecept()方法 + 执行target上f()方法。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set&lt;Method&gt; methods = signatureMap.get(method.getDeclaringClass());
if (methods != null &amp;&amp; methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
private static Map&lt;Class&lt;?&gt;, Set&lt;Method&gt;&gt; getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException(&quot;No @Intercepts annotation was found in interceptor &quot; + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map&lt;Class&lt;?&gt;, Set&lt;Method&gt;&gt; signatureMap = new HashMap&lt;Class&lt;?&gt;, Set&lt;Method&gt;&gt;();
for (Signature sig : sigs) {
Set&lt;Method&gt; methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet&lt;Method&gt;();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException(&quot;Could not find method on &quot; + sig.type() + &quot; named &quot; + sig.method() + &quot;. Cause: &quot; + e, e);
}
}
return signatureMap;
}
private static Class&lt;?&gt;[] getAllInterfaces(Class&lt;?&gt; type, Map&lt;Class&lt;?&gt;, Set&lt;Method&gt;&gt; signatureMap) {
Set&lt;Class&lt;?&gt;&gt; interfaces = new HashSet&lt;Class&lt;?&gt;&gt;();
while (type != null) {
for (Class&lt;?&gt; c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class&lt;?&gt;[interfaces.size()]);
}
}
```
实际上Plugin是借助Java InvocationHandler实现的动态代理类。用来代理给target对象添加Interceptor功能。其中要代理的target对象就是Executor、StatementHandler、ParameterHandler、ResultSetHandler这四个类的对象。wrap()静态方法是一个工具函数用来生成target对象的动态代理对象。
当然只有interceptor与target互相匹配的时候wrap()方法才会返回代理对象否则就返回target对象本身。怎么才算是匹配呢那就是interceptor通过@Signature注解要拦截的类包含target对象具体可以参看wrap()函数的代码实现上面一段代码中的第16~19行
MyBatis中的职责链模式的实现方式比较特殊。它对同一个目标对象嵌套多次代理也就是InteceptorChain中的pluginAll()函数要执行的任务。每个代理对象Plugin对象代理一个拦截器Interceptor对象功能。为了方便你查看我将pluginAll()函数的代码又拷贝到了下面。
```
public Object pluginAll(Object target) {
// 嵌套代理
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
// 上面这行代码等于下面这行代码target(代理对象)=target(目标对象)+interceptor(拦截器功能)
// target = Plugin.wrap(target, interceptor);
}
return target;
}
// MyBatis像下面这样创建target(Executor、StatementHandler、ParameterHandler、ResultSetHandler相当于多次嵌套代理
Object target = interceptorChain.pluginAll(target);
```
当执行Executor、StatementHandler、ParameterHandler、ResultSetHandler这四个类上的某个方法的时候MyBatis会嵌套执行每层代理对象Plugin对象上的invoke()方法。而invoke()方法会先执行代理对象中的interceptor的intecept()函数然后再执行被代理对象上的方法。就这样一层一层地把代理对象上的intercept()函数执行完之后MyBatis才最终执行那4个原始类对象上的方法。
## 重点回顾
好了,今天内容到此就讲完了。我们来一块总结回顾一下,你需要重点掌握的内容。
今天我们带你剖析了如何利用职责链模式和动态代理模式来实现MyBatis Plugin。至此我们就已经学习了三种职责链常用的应用场景过滤器Servlet Filter、拦截器Spring Interceptor、插件MyBatis Plugin
职责链模式的实现一般包含处理器和处理器链两部分。这两个部分对应到Servlet Filter的源码就是Filter和FilterChain对应到Spring Interceptor的源码就是HandlerInterceptor和HandlerExecutionChain对应到MyBatis Plugin的源码就是Interceptor和InterceptorChain。除此之外MyBatis Plugin还包含另外一个非常重要的类Plugin类。它用来生成被拦截对象的动态代理。
在这三种应用场景中职责链模式的实现思路都不大一样。其中Servlet Filter采用递归来实现拦截方法前后添加逻辑。Spring Interceptor的实现比较简单把拦截方法前后要添加的逻辑放到两个方法中实现。MyBatis Plugin采用嵌套动态代理的方法来实现实现思路很有技巧。
## 课堂讨论
Servlet Filter、Spring Interceptor可以用来拦截用户自己定义的类的方法而MyBatis Plugin默认可以拦截的只有Executor、StatementHandler、ParameterHandler、ResultSetHandler这四个类的方法而且这四个类是MyBatis实现的类并非用户自己定义的类。那MyBatis Plugin为什么不像Servlet Filter、Spring Interceptor那样提供拦截用户自定义类的方法的功能呢
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。

View File

@@ -0,0 +1,678 @@
<audio id="audio" title="89 | 开源实战五总结MyBatis框架中用到的10种设计模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8c/57/8cf480d2711c845dbd7ea62681bebd57.mp3"></audio>
上节课我带你剖析了利用职责链模式和动态代理模式实现MyBatis Plugin。至此我们已经学习了三种职责链常用的应用场景过滤器Servlet Filter、拦截器Spring Interceptor、插件MyBatis Plugin
今天我们再对MyBatis用到的设计模式做一个总结。它用到的设计模式也不少就我所知的不下十几种。有些我们前面已经讲到有些比较简单。有了前面这么多讲的学习和训练我想你现在应该已经具备了一定的研究和分析能力能够自己做查缺补漏把提到的所有源码都搞清楚。所以在今天的课程中如果有哪里有疑问你尽可以去查阅源码自己先去学习一下有不懂的地方再到评论区和大家一起交流。
话不多说,让我们正式开始今天的学习吧!
## SqlSessionFactoryBuilder为什么要用建造者模式来创建SqlSessionFactory
在[第87讲](https://time.geekbang.org/column/article/239239)中我们通过一个查询用户的例子展示了用MyBatis进行数据库编程。为了方便你查看我把相关的代码重新摘抄到这里。
```
public class MyBatisDemo {
public static void main(String[] args) throws IOException {
Reader reader = Resources.getResourceAsReader(&quot;mybatis.xml&quot;);
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sessionFactory.openSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
UserDo userDo = userMapper.selectById(8);
//...
}
}
```
针对这段代码,请你思考一下下面这个问题。
之前讲到建造者模式的时候我们使用Builder类来创建对象一般都是先级联一组setXXX()方法来设置属性然后再调用build()方法最终创建对象。但是在上面这段代码中通过SqlSessionFactoryBuilder来创建SqlSessionFactory并不符合这个套路。它既没有setter方法而且build()方法也并非无参需要传递参数。除此之外从上面的代码来看SqlSessionFactory对象的创建过程也并不复杂。那直接通过构造函数来创建SqlSessionFactory不就行了吗为什么还要借助建造者模式创建SqlSessionFactory呢
要回答这个问题我们就要先看下SqlSessionFactoryBuilder类的源码。我把源码摘抄到了这里如下所示
```
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(&quot;Error building SqlSession.&quot;, 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(&quot;Error building SqlSession.&quot;, 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);
}
}
```
SqlSessionFactoryBuilder类中有大量的build()重载函数。为了方便你查看以及待会儿跟SqlSessionFactory类的代码作对比我把重载函数定义抽象出来贴到这里。
```
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader);
public SqlSessionFactory build(Reader reader, String environment);
public SqlSessionFactory build(Reader reader, Properties properties);
public SqlSessionFactory build(Reader reader, String environment, Properties properties);
public SqlSessionFactory build(InputStream inputStream);
public SqlSessionFactory build(InputStream inputStream, String environment);
public SqlSessionFactory build(InputStream inputStream, Properties properties);
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);
// 上面所有的方法最终都调用这个方法
public SqlSessionFactory build(Configuration config);
}
```
我们知道如果一个类包含很多成员变量而构建对象并不需要设置所有的成员变量只需要选择性地设置其中几个就可以。为了满足这样的构建需求我们就要定义多个包含不同参数列表的构造函数。为了避免构造函数过多、参数列表过长我们一般通过无参构造函数加setter方法或者通过建造者模式来解决。
从建造者模式的设计初衷上来看SqlSessionFactoryBuilder虽然带有Builder后缀但不要被它的名字所迷惑它并不是标准的建造者模式。一方面原始类SqlSessionFactory的构建只需要一个参数并不复杂。另一方面Builder类SqlSessionFactoryBuilder仍然定义了n多包含不同参数列表的构造函数。
实际上SqlSessionFactoryBuilder设计的初衷只不过是为了简化开发。因为构建SqlSessionFactory需要先构建Configuration而构建Configuration是非常复杂的需要做很多工作比如配置的读取、解析、创建n多对象等。为了将构建SqlSessionFactory的过程隐藏起来对程序员透明MyBatis就设计了SqlSessionFactoryBuilder类封装这些构建细节。
## SqlSessionFactory到底属于工厂模式还是建造器模式
在刚刚那段MyBatis示例代码中我们通过SqlSessionFactoryBuilder创建了SqlSessionFactory然后再通过SqlSessionFactory创建了SqlSession。刚刚我们讲了SqlSessionFactoryBuilder现在我们再来看下SqlSessionFactory。
从名字上你可能已经猜到SqlSessionFactory是一个工厂类用到的设计模式是工厂模式。不过它跟SqlSessionFactoryBuilder类似名字有很大的迷惑性。实际上它也并不是标准的工厂模式。为什么这么说呢我们先来看下SqlSessionFactory类的源码。
```
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
```
SqlSessionFactory是一个接口DefaultSqlSessionFactory是它唯一的实现类。DefaultSqlSessionFactory源码如下所示
```
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
@Override
public SqlSession openSession(boolean autoCommit) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}
@Override
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
@Override
public SqlSession openSession(TransactionIsolationLevel level) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), level, false);
}
@Override
public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
return openSessionFromDataSource(execType, level, false);
}
@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
return openSessionFromDataSource(execType, null, autoCommit);
}
@Override
public SqlSession openSession(Connection connection) {
return openSessionFromConnection(configuration.getDefaultExecutorType(), connection);
}
@Override
public SqlSession openSession(ExecutorType execType, Connection connection) {
return openSessionFromConnection(execType, connection);
}
@Override
public Configuration getConfiguration() {
return configuration;
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException(&quot;Error opening session. Cause: &quot; + e, e);
} finally {
ErrorContext.instance().reset();
}
}
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
boolean autoCommit;
try {
autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
// Failover to true, as most poor drivers
// or databases won't support transactions
autoCommit = true;
}
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
final Transaction tx = transactionFactory.newTransaction(connection);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException(&quot;Error opening session. Cause: &quot; + e, e);
} finally {
ErrorContext.instance().reset();
}
}
//...省略部分代码...
}
```
从SqlSessionFactory和DefaultSqlSessionFactory的源码来看它的设计非常类似刚刚讲到的SqlSessionFactoryBuilder通过重载多个openSession()函数支持通过组合autoCommit、Executor、Transaction等不同参数来创建SqlSession对象。标准的工厂模式通过type来创建继承同一个父类的不同子类对象而这里只不过是通过传递进不同的参数来创建同一个类的对象。所以它更像建造者模式。
虽然设计思路基本一致但一个叫xxxBuilderSqlSessionFactoryBuilder一个叫xxxFactorySqlSessionFactory。而且叫xxxBuilder的也并非标准的建造者模式叫xxxFactory的也并非标准的工厂模式。所以我个人觉得MyBatis对这部分代码的设计还是值得优化的。
实际上这两个类的作用只不过是为了创建SqlSession对象没有其他作用。所以我更建议参照Spring的设计思路把SqlSessionFactoryBuilder和SqlSessionFactory的逻辑放到一个叫“ApplicationContext”的类中。让这个类来全权负责读入配置文件创建Congfiguration生成SqlSession。
## BaseExecutor模板模式跟普通的继承有什么区别
如果去查阅SqlSession与DefaultSqlSession的源码你会发现SqlSession执行SQL的业务逻辑都是委托给了Executor来实现。Executor相关的类主要是用来执行SQL。其中Executor本身是一个接口BaseExecutor是一个抽象类实现了Executor接口而BatchExecutor、SimpleExecutor、ReuseExecutor三个类继承BaseExecutor抽象类。
那BatchExecutor、SimpleExecutor、ReuseExecutor三个类跟BaseExecutor是简单的继承关系还是模板模式关系呢怎么来判断呢我们看一下BaseExecutor的源码就清楚了。
```
public abstract class BaseExecutor implements Executor {
//...省略其他无关代码...
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity(&quot;executing an update&quot;).object(ms.getId());
if (closed) {
throw new ExecutorException(&quot;Executor was closed.&quot;);
}
clearLocalCache();
return doUpdate(ms, parameter);
}
public List&lt;BatchResult&gt; flushStatements(boolean isRollBack) throws SQLException {
if (closed) {
throw new ExecutorException(&quot;Executor was closed.&quot;);
}
return doFlushStatements(isRollBack);
}
private &lt;E&gt; List&lt;E&gt; queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List&lt;E&gt; list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
@Override
public &lt;E&gt; Cursor&lt;E&gt; queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
return doQueryCursor(ms, parameter, rowBounds, boundSql);
}
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List&lt;BatchResult&gt; doFlushStatements(boolean isRollback) throws SQLException;
protected abstract &lt;E&gt; List&lt;E&gt; doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract &lt;E&gt; Cursor&lt;E&gt; doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;
}
```
模板模式基于继承来实现代码复用。如果抽象类中包含模板方法模板方法调用有待子类实现的抽象方法那这一般就是模板模式的代码实现。而且在命名上模板方法与抽象方法一般是一一对应的抽象方法在模板方法前面多一个“do”比如在BaseExecutor类中其中一个模板方法叫update()那对应的抽象方法就叫doUpdate()。
## SqlNode如何利用解释器模式来解析动态SQL
支持配置文件中编写动态SQL是MyBatis一个非常强大的功能。所谓动态SQL就是在SQL中可以包含在trim、if、#{}等语法标签在运行时根据条件来生成不同的SQL。这么说比较抽象我举个例子解释一下。
```
&lt;update id=&quot;update&quot; parameterType=&quot;com.xzg.cd.a89.User&quot;
UPDATE user
&lt;trim prefix=&quot;SET&quot; prefixOverrides=&quot;,&quot;&gt;
&lt;if test=&quot;name != null and name != ''&quot;&gt;
name = #{name}
&lt;/if&gt;
&lt;if test=&quot;age != null and age != ''&quot;&gt;
, age = #{age}
&lt;/if&gt;
&lt;if test=&quot;birthday != null and birthday != ''&quot;&gt;
, birthday = #{birthday}
&lt;/if&gt;
&lt;/trim&gt;
where id = ${id}
&lt;/update&gt;
```
显然动态SQL的语法规则是MyBatis自定义的。如果想要根据语法规则替换掉动态SQL中的动态元素生成真正可以执行的SQL语句MyBatis还需要实现对应的解释器。这一部分功能就可以看做是解释器模式的应用。实际上如果你去查看它的代码实现你会发现它跟我们在前面讲解释器模式时举的那两个例子的代码结构非常相似。
我们前面提到解释器模式在解释语法规则的时候一般会把规则分割成小的单元特别是可以嵌套的小单元针对每个小单元来解析最终再把解析结果合并在一起。这里也不例外。MyBatis把每个语法小单元叫SqlNode。SqlNode的定义如下所示
```
public interface SqlNode {
boolean apply(DynamicContext context);
}
```
对于不同的语法小单元MyBatis定义不同的SqlNode实现类。
<img src="https://static001.geekbang.org/resource/image/03/9f/0365945b91a00e3b98d0c09b2665f59f.png" alt="">
整个解释器的调用入口在DynamicSqlSource.getBoundSql方法中它调用了rootSqlNode.apply(context)方法。因为整体的代码结构跟[第72讲](https://time.geekbang.org/column/article/225904)中的例子基本一致所以每个SqlNode实现类的代码我就不带你一块阅读了感兴趣的话你可以自己去看下。
## ErrorContext如何实现一个线程唯一的单例模式
在单例模式那一部分我们讲到单例模式是进程唯一的。同时我们还讲到单例模式的几种变形比如线程唯一的单例、集群唯一的单例等。在MyBatis中ErrorContext这个类就是标准单例的变形线程唯一的单例。
它的代码实现我贴到下面了。它基于Java中的ThreadLocal来实现。如果不熟悉ThreadLocal你可以回过头去看下[第43讲](https://time.geekbang.org/column/article/196790)中线程唯一的单例的实现方法。实际上这里的ThreadLocal就相当于那里的ConcurrentHashMap。
```
public class ErrorContext {
private static final String LINE_SEPARATOR = System.getProperty(&quot;line.separator&quot;,&quot;\n&quot;);
private static final ThreadLocal&lt;ErrorContext&gt; LOCAL = new ThreadLocal&lt;ErrorContext&gt;();
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
private ErrorContext() {
}
public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
}
```
## Cache为什么要用装饰器模式而不设计成继承子类
我们前面提到MyBatis是一个ORM框架。实际上它不只是简单地完成了对象和数据库数据之间的互相转化还提供了很多其他功能比如缓存、事务等。接下来我们再讲讲它的缓存实现。
在MyBatis中缓存功能由接口Cache定义。PerpetualCache类是最基础的缓存类是一个大小无限的缓存。除此之外MyBatis还设计了9个包裹PerpetualCache类的装饰器类用来实现功能增强。它们分别是FifoCache、LoggingCache、LruCache、ScheduledCache、SerializedCache、SoftCache、SynchronizedCache、WeakCache、TransactionalCache。
```
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
ReadWriteLock getReadWriteLock();
}
public class PerpetualCache implements Cache {
private final String id;
private Map&lt;Object, Object&gt; cache = new HashMap&lt;Object, Object&gt;();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
//省略部分代码...
}
```
这9个装饰器类的代码结构都类似我只将其中的LruCache的源码贴到这里。从代码中我们可以看出它是标准的装饰器模式的代码实现。
```
public class LruCache implements Cache {
private final Cache delegate;
private Map&lt;Object, Object&gt; 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&lt;Object, Object&gt;(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry&lt;Object, Object&gt; eldest) {
boolean tooBig = size() &gt; 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;
}
}
}
```
之所以MyBatis采用装饰器模式来实现缓存功能是因为装饰器模式采用了组合而非继承更加灵活能够有效地避免继承关系的组合爆炸。关于这一点你可以回过头去看下[第10讲](https://time.geekbang.org/column/article/169593)的内容。
## PropertyTokenizer如何利用迭代器模式实现一个属性解析器
前面我们讲到迭代器模式常用来替代for循环遍历集合元素。Mybatis的PropertyTokenizer类实现了Java Iterator接口是一个迭代器用来对配置属性进行解析。具体的代码如下所示
```
// person[0].birthdate.year 会被分解为3个PropertyTokenizer对象。其中第一个PropertyTokenizer对象的各个属性值如注释所示。
public class PropertyTokenizer implements Iterator&lt;PropertyTokenizer&gt; {
private String name; // person
private final String indexedName; // person[0]
private String index; // 0
private final String children; // birthdate.year
public PropertyTokenizer(String fullname) {
int delim = fullname.indexOf('.');
if (delim &gt; -1) {
name = fullname.substring(0, delim);
children = fullname.substring(delim + 1);
} else {
name = fullname;
children = null;
}
indexedName = name;
delim = name.indexOf('[');
if (delim &gt; -1) {
index = name.substring(delim + 1, name.length() - 1);
name = name.substring(0, delim);
}
}
public String getName() {
return name;
}
public String getIndex() {
return index;
}
public String getIndexedName() {
return indexedName;
}
public String getChildren() {
return children;
}
@Override
public boolean hasNext() {
return children != null;
}
@Override
public PropertyTokenizer next() {
return new PropertyTokenizer(children);
}
@Override
public void remove() {
throw new UnsupportedOperationException(&quot;Remove is not supported, as it has no meaning in the context of properties.&quot;);
}
}
```
实际上PropertyTokenizer类也并非标准的迭代器类。它将配置的解析、解析之后的元素、迭代器这三部分本该放到三个类中的代码都耦合在一个类中所以看起来稍微有点难懂。不过这样做的好处是能够做到惰性解析。我们不需要事先将整个配置解析成多个PropertyTokenizer对象。只有当我们在调用next()函数的时候,才会解析其中部分配置。
## Log如何使用适配器模式来适配不同的日志框架
在适配器模式那节课中我们讲过Slf4j框架为了统一各个不同的日志框架Log4j、JCL、Logback等提供了一套统一的日志接口。不过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);
}
```
针对Log接口MyBatis还提供了各种不同的实现类分别使用不同的日志框架来实现Log接口。
<img src="https://static001.geekbang.org/resource/image/95/14/95946f9e9c524cc06279114f7a654f14.png" alt="">
这几个实现类的代码结构基本上一致。我把其中的Log4jImpl的源码贴到了这里。我们知道在适配器模式中传递给适配器构造函数的是被适配的类对象而这里是clazz相当于日志名称name所以从代码实现上来讲它并非标准的适配器模式。但是从应用场景上来看这里确实又起到了适配的作用是典型的适配器模式的应用场景。
```
import org.apache.ibatis.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private final 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中用到的8种设计模式它们分别是建造者模式、工厂模式、模板模式、解释器模式、单例模式、装饰器模式、迭代器模式、适配器模式。加上上一节课中讲到的职责链和动态代理我们总共讲了10种设计模式。
还是那句老话你不需要记忆哪个类用到了哪个模式因为不管你看多少遍甚至记住并没有什么用。我希望你不仅仅只是把文章看了更希望你能动手把MyBatis源码下载下来自己去阅读一下相关的源码锻炼自己阅读源码的能力。这比单纯看文章效果要好很多倍。
除此之外从这两节课的讲解中不知道你有没有发现MyBatis对很多设计模式的实现都并非标准的代码实现都做了比较多的自我改进。实际上这就是所谓的灵活应用只借鉴不照搬根据具体问题针对性地去解决。
## 课堂讨论
今天我们提到SqlSessionFactoryBuilder跟SqlSessionFactory虽然名字后缀不同但是设计思路一致都是为了隐藏SqlSession的创建细节。从这一点上来看命名有点不够统一。而且我们还提到SqlSessionFactoryBuilder并非标准的建造者模式SqlSessionFactory也并非标准的工厂模式。对此你有什么看法呢
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。