mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 14:43:42 +08:00
del
This commit is contained in:
@@ -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<Calendar> {
|
||||
//...
|
||||
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("ca");
|
||||
if (caltype != null) {
|
||||
switch (caltype) {
|
||||
case "buddhist":
|
||||
cal = new BuddhistCalendar(zone, aLocale);
|
||||
break;
|
||||
case "japanese":
|
||||
cal = new JapaneseImperialCalendar(zone, aLocale);
|
||||
break;
|
||||
case "gregory":
|
||||
cal = new GregorianCalendar(zone, aLocale);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cal == null) {
|
||||
if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
|
||||
cal = new BuddhistCalendar(zone, aLocale);
|
||||
} else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja" && aLocale.getCountry() == "JP") {
|
||||
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<Calendar> {
|
||||
//...
|
||||
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("ca");
|
||||
}
|
||||
if (type == null) {
|
||||
if (locale.getCountry() == "TH" && locale.getLanguage() == "th") {
|
||||
type = "buddhist";
|
||||
} else {
|
||||
type = "gregory";
|
||||
}
|
||||
}
|
||||
switch (type) {
|
||||
case "gregory":
|
||||
cal = new GregorianCalendar(zone, locale, true);
|
||||
break;
|
||||
case "iso8601":
|
||||
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 "buddhist":
|
||||
cal = new BuddhistCalendar(zone, locale);
|
||||
cal.clear();
|
||||
break;
|
||||
case "japanese":
|
||||
cal = new JapaneseImperialCalendar(zone, locale, true);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unknown calendar type: " + 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) && fields[WEEK_YEAR] > fields[YEAR];
|
||||
if (weekDate && !cal.isWeekDateSupported()) {
|
||||
throw new IllegalArgumentException("week date is unsupported by " + type);
|
||||
}
|
||||
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
|
||||
for (int index = 0; index <= 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 <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) {
|
||||
return new UnmodifiableCollection<>(c);
|
||||
}
|
||||
|
||||
static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
|
||||
private static final long serialVersionUID = 1820017752578914078L;
|
||||
final Collection<? extends E> c;
|
||||
|
||||
UnmodifiableCollection(Collection<? extends E> 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 <T> T[] toArray(T[] a) {return c.toArray(a);}
|
||||
public String toString() {return c.toString();}
|
||||
|
||||
public Iterator<E> iterator() {
|
||||
return new Iterator<E>() {
|
||||
private final Iterator<? extends E> 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<? super E> 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<?> coll) {
|
||||
return c.containsAll(coll);
|
||||
}
|
||||
public boolean addAll(Collection<? extends E> coll) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
public boolean removeAll(Collection<?> coll) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
public boolean retainAll(Collection<?> coll) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
public void clear() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
// Override default methods in Collection
|
||||
@Override
|
||||
public void forEach(Consumer<? super E> action) {
|
||||
c.forEach(action);
|
||||
}
|
||||
@Override
|
||||
public boolean removeIf(Predicate<? super E> filter) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Spliterator<E> spliterator() {
|
||||
return (Spliterator<E>)c.spliterator();
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Stream<E> stream() {
|
||||
return (Stream<E>)c.stream();
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Stream<E> parallelStream() {
|
||||
return (Stream<E>)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 <T> 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 <T> Enumeration<T> enumeration(final Collection<T> c) {
|
||||
return new Enumeration<T>() {
|
||||
private final Iterator<T> i = c.iterator();
|
||||
|
||||
public boolean hasMoreElements() {
|
||||
return i.hasNext();
|
||||
}
|
||||
|
||||
public T nextElement() {
|
||||
return i.next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 重点回顾
|
||||
|
||||
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
|
||||
|
||||
今天,我重点讲了工厂模式、建造者模式、装饰器模式、适配器模式,这四种模式在Java JDK中的应用,主要目的是给你展示真实项目中是如何灵活应用设计模式的。
|
||||
|
||||
从今天的讲解中,我们可以学习到,尽管在之前的理论讲解中,我们都有讲到每个模式的经典代码实现,但是,在真实的项目开发中,这些模式的应用更加灵活,代码实现更加自由,可以根据具体的业务场景、功能需求,对代码实现做很大的调整,甚至还可能会对模式本身的设计思路做调整。
|
||||
|
||||
比如,Java JDK中的Calendar类,就耦合了业务功能代码、工厂方法、建造者类三种类型的代码,而且,在建造者类的build()方法中,前半部分是工厂方法的代码实现,后半部分才是真正的建造者模式的代码实现。这也告诉我们,在项目中应用设计模式,切不可生搬硬套,过于学院派,要学会结合实际情况做灵活调整,做到心中无剑胜有剑。
|
||||
|
||||
## 课堂讨论
|
||||
|
||||
在Java中,经常用到的StringBuilder类是否是建造者模式的应用呢?你可以试着像我一样从源码的角度去剖析一下。
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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<Student> students = new ArrayList<>();
|
||||
students.add(new Student("Alice", 19, 89.0f));
|
||||
students.add(new Student("Peter", 20, 78.0f));
|
||||
students.add(new Student("Leo", 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<Student> students) {
|
||||
for (Student s : students) {
|
||||
System.out.println(s.getName() + " " + s.getAge() + " " + s.getScore());
|
||||
}
|
||||
}
|
||||
|
||||
public static class AgeAscComparator implements Comparator<Student> {
|
||||
@Override
|
||||
public int compare(Student o1, Student o2) {
|
||||
return o1.getAge() - o2.getAge();
|
||||
}
|
||||
}
|
||||
|
||||
public static class NameAscComparator implements Comparator<Student> {
|
||||
@Override
|
||||
public int compare(Student o1, Student o2) {
|
||||
return o1.getName().compareTo(o2.getName());
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScoreDescComparator implements Comparator<Student> {
|
||||
@Override
|
||||
public int compare(Student o1, Student o2) {
|
||||
if (Math.abs(o1.getScore() - o2.getScore()) < 0.001) {
|
||||
return 0;
|
||||
} else if (o1.getScore() < 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<Observer> obs;
|
||||
|
||||
public Observable() {
|
||||
obs = new Vector<>();
|
||||
}
|
||||
|
||||
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>=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
|
||||
* <code>Runtime</code> that allows the application to interface with
|
||||
* the environment in which the application is running. The current
|
||||
* runtime can be obtained from the <code>getRuntime</code> method.
|
||||
* <p>
|
||||
* 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("shutdownHooks"));
|
||||
}
|
||||
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成员变量是否多此一举?
|
||||
|
||||
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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原则与为扩展而设计,这都需要我们根据实际情况去权衡。
|
||||
|
||||
## 课堂讨论
|
||||
|
||||
从设计原则和思想的角度来看,你觉得哪些原则或思想在大型软件开发中最能发挥作用,最能有效地应对代码的复杂性?
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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分,最后又没有很好的考核机制,好坏大家也都不知道。所以,一句话总结一下:切忌敏于言而讷于行。
|
||||
|
||||
除此之外,我们刚刚讲的所有方法都治标不治本。软件开发过程中的问题往往千奇百怪。要想每个问题都能顺利解决,除了理论知识和经验之外,更重要的是要具备分析问题、解决问题的能力。这也是为什么很多公司很重视应届生招聘,希望从一开始就招聘一些有潜力的员工。找到对的人、用对好的人,打造优秀的技术文化,才是一直保持卓越的根本。
|
||||
|
||||
## 课堂讨论
|
||||
|
||||
从研发管理和开发技巧的角度,你还有哪些能够有效保持项目代码质量的经验,可以分享给大家?
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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 Review,Code 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 Board(Code 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 cheap,show 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 Review,Code Review的价值在哪里。我的总结如下:Code Review践行“三人行必有我师”、能摒弃“个人英雄主义”、能有效提高代码可读性、是技术传帮带的有效途径、能保证代码不止一个人熟悉、能打造良好的技术氛围、是一种技术沟通方式、能提高团队的自律性。
|
||||
|
||||
除此之外,我还对Code Review在落地执行过程中的一些问题,做了简单的答疑。我这里就不再重复罗列了。如果你在Code Review过程中遇到同样的问题,希望我的建议对你有所帮助。
|
||||
|
||||
## 课堂讨论
|
||||
|
||||
对是否应该做Code Review,你有什么看法呢?你所在的公司是否有严格的Code Review呢?在Code Review的过程中,你又遇到了哪些问题?
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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公司开发经验的一手资料,特别是在单元测试、编码规范方面。
|
||||
|
||||
## 课堂讨论
|
||||
|
||||
针对你正在参与开发的项目,思考一下,有哪些通用的功能模块可以抽象出来,设计开发成独立的类库、框架、功能组件?它们都可能会包括哪些功能点呢?试着自己设计一下吧!
|
||||
|
||||
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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<String, String> cache = CacheBuilder.newBuilder()
|
||||
.initialCapacity(100)
|
||||
.maximumSize(1000)
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
cache.put("key1", "value1");
|
||||
String value = cache.getIfPresent("key1");
|
||||
System.out.println(value);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从上面的代码中,我们可以发现,Cache对象是通过CacheBuilder这样一个Builder类来创建的。为什么要由Builder类来创建Cache对象呢?我想这个问题应该对你来说没难度了吧。
|
||||
|
||||
你可以先想一想,然后再来看我的回答。构建一个缓存,需要配置n多参数,比如过期时间、淘汰策略、最大缓存大小等等。相应地,Cache类就会包含n多成员变量。我们需要在构造函数中,设置这些成员变量的值,但又不是所有的值都必须设置,设置哪些值由用户来决定。为了满足这个需求,我们就需要定义多个包含不同参数列表的构造函数。
|
||||
|
||||
为了避免构造函数的参数列表过长、不同的构造函数过多,我们一般有两种解决方案。其中,一个解决方案是使用Builder模式;另一个方案是先通过无参构造函数创建对象,然后再通过setXXX()方法来逐一设置需要的设置的成员变量。
|
||||
|
||||
那我再问你一个问题,为什么Guava选择第一种而不是第二种解决方案呢?使用第二种解决方案是否也可以呢?答案是不行的。至于为什么,我们看下源码就清楚了。我把CacheBuilder类中的build()函数摘抄到了下面,你可以先看下。
|
||||
|
||||
```
|
||||
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
|
||||
this.checkWeightWithWeigher();
|
||||
this.checkNonLoadingCache();
|
||||
return new LocalManualCache(this);
|
||||
}
|
||||
|
||||
private void checkNonLoadingCache() {
|
||||
Preconditions.checkState(this.refreshNanos == -1L, "refreshAfterWrite requires a LoadingCache");
|
||||
}
|
||||
|
||||
private void checkWeightWithWeigher() {
|
||||
if (this.weigher == null) {
|
||||
Preconditions.checkState(this.maximumWeight == -1L, "maximumWeight requires weigher");
|
||||
} else if (this.strictParsing) {
|
||||
Preconditions.checkState(this.maximumWeight != -1L, "weigher requires maximumWeight");
|
||||
} else if (this.maximumWeight == -1L) {
|
||||
logger.log(Level.WARNING, "ignoring weigher specified without maximumWeight");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
看了代码,你是否有了答案呢?实际上,答案我们在讲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<E> extends ForwardingObject implements Collection<E> {
|
||||
protected ForwardingCollection() {
|
||||
}
|
||||
|
||||
protected abstract Collection<E> delegate();
|
||||
|
||||
public Iterator<E> iterator() {
|
||||
return this.delegate().iterator();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return this.delegate().size();
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
public boolean removeAll(Collection<?> 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<?> collection) {
|
||||
return this.delegate().containsAll(collection);
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
public boolean addAll(Collection<? extends E> collection) {
|
||||
return this.delegate().addAll(collection);
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
public boolean retainAll(Collection<?> collection) {
|
||||
return this.delegate().retainAll(collection);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.delegate().clear();
|
||||
}
|
||||
|
||||
public Object[] toArray() {
|
||||
return this.delegate().toArray();
|
||||
}
|
||||
|
||||
//...省略部分代码...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
光看ForwardingCollection的代码实现,你可能想不到它的作用。我再给点提示,举一个它的用法示例,如下所示:
|
||||
|
||||
```
|
||||
public class AddLoggingCollection<E> extends ForwardingCollection<E> {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AddLoggingCollection.class);
|
||||
private Collection<E> originalCollection;
|
||||
|
||||
public AddLoggingCollection(Collection<E> originalCollection) {
|
||||
this.originalCollection = originalCollection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection delegate() {
|
||||
return this.originalCollection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(E element) {
|
||||
logger.info("Add element: " + element);
|
||||
return this.delegate().add(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends E> collection) {
|
||||
logger.info("Size of elements to add: " + 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<String> originalList = new ArrayList<>();
|
||||
originalList.add("a");
|
||||
originalList.add("b");
|
||||
originalList.add("c");
|
||||
|
||||
List<String> jdkUnmodifiableList = Collections.unmodifiableList(originalList);
|
||||
List<String> guavaImmutableList = ImmutableList.copyOf(originalList);
|
||||
|
||||
//jdkUnmodifiableList.add("d"); // 抛出UnsupportedOperationException
|
||||
// guavaImmutableList.add("d"); // 抛出UnsupportedOperationException
|
||||
originalList.add("d");
|
||||
|
||||
print(originalList); // a b c d
|
||||
print(jdkUnmodifiableList); // a b c d
|
||||
print(guavaImmutableList); // a b c
|
||||
}
|
||||
|
||||
private static void print(List<String> list) {
|
||||
for (String s : list) {
|
||||
System.out.print(s + " ");
|
||||
}
|
||||
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的不变集合的数据并没有增加。这是两者最大的区别。那这两者底层分别是如何实现不变的呢?
|
||||
|
||||
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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<Integer> result = Stream.of("f", "ba", "hello")
|
||||
.map(s -> s.length())
|
||||
.filter(l -> l <= 3)
|
||||
.max((o1, o2) -> 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<Integer> result = Stream.of("f", "ba", "hello") // of返回Stream<String>对象
|
||||
.map(s -> s.length()) // map返回Stream<Integer>对象
|
||||
.filter(l -> l <= 3) // filter返回Stream<Integer>对象
|
||||
.max((o1, o2) -> o1-o2); // max终止操作:返回Optional<Integer>
|
||||
System.out.println(result.get()); // 输出2
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**其次,我们再来看下Lambda表达式。**
|
||||
|
||||
我们前面讲到,Java引入Lambda表达式的主要作用是简化代码编写。实际上,我们也可以不用Lambda表达式来书写例子中的代码。我们拿其中的map函数来举例说明一下。
|
||||
|
||||
下面有三段代码,第一段代码展示了map函数的定义,实际上,map函数接收的参数是一个Function接口,也就是待会儿要讲到的函数接口。第二段代码展示了map函数的使用方式。第三段代码是针对第二段代码用Lambda表达式简化之后的写法。实际上,Lambda表达式在Java中只是一个语法糖而已,底层是基于函数接口来实现的,也就是第二段代码展示的写法。
|
||||
|
||||
```
|
||||
// Stream中map函数的定义:
|
||||
public interface Stream<T> extends BaseStream<T, Stream<T>> {
|
||||
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
|
||||
//...省略其他函数...
|
||||
}
|
||||
|
||||
// Stream中map的使用方法:
|
||||
Stream.of("fo", "bar", "hello").map(new Function<String, Integer>() {
|
||||
@Override
|
||||
public Integer apply(String s) {
|
||||
return s.length();
|
||||
}
|
||||
});
|
||||
|
||||
// 用Lambda表达式简化后的写法:
|
||||
Stream.of("fo", "bar", "hello").map(s -> s.length());
|
||||
|
||||
```
|
||||
|
||||
Lambda表达式语法不是我们学习的重点。我这里只稍微介绍一下。如果感兴趣,你可以自行深入研究。
|
||||
|
||||
Lambda表达式包括三部分:输入、函数体、输出。表示出来的话就是下面这个样子:
|
||||
|
||||
```
|
||||
(a, b) -> { 语句1; 语句2;...; return 输出; } //a,b是输入参数
|
||||
|
||||
```
|
||||
|
||||
实际上,Lambda表达式的写法非常灵活。我们刚刚给出的是标准写法,还有很多简化写法。比如,如果输入参数只有一个,可以省略(),直接写成a->{…};如果没有入参,可以直接将输入和箭头都省略掉,只保留函数体;如果函数体只有一个语句,那可以将{}省略掉;如果函数没有返回值,return语句就可以不用写了。
|
||||
|
||||
如果我们把之前例子中的Lambda表达式,全部替换为函数接口的实现方式,就是下面这样子的。代码是不是多了很多?
|
||||
|
||||
```
|
||||
Optional<Integer> result = Stream.of("f", "ba", "hello")
|
||||
.map(s -> s.length())
|
||||
.filter(l -> l <= 3)
|
||||
.max((o1, o2) -> o1-o2);
|
||||
|
||||
// 还原为函数接口的实现方式
|
||||
Optional<Integer> result2 = Stream.of("fo", "bar", "hello")
|
||||
.map(new Function<String, Integer>() {
|
||||
@Override
|
||||
public Integer apply(String s) {
|
||||
return s.length();
|
||||
}
|
||||
})
|
||||
.filter(new Predicate<Integer>() {
|
||||
@Override
|
||||
public boolean test(Integer l) {
|
||||
return l <= 3;
|
||||
}
|
||||
})
|
||||
.max(new Comparator<Integer>() {
|
||||
@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<T, R> {
|
||||
R apply(T t); // 只有这一个未实现的方法
|
||||
|
||||
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
|
||||
Objects.requireNonNull(before);
|
||||
return (V v) -> apply(before.apply(v));
|
||||
}
|
||||
|
||||
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
|
||||
Objects.requireNonNull(after);
|
||||
return (T t) -> after.apply(apply(t));
|
||||
}
|
||||
|
||||
static <T> Function<T, T> identity() {
|
||||
return t -> t;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Predicate<T> {
|
||||
boolean test(T t); // 只有这一个未实现的方法
|
||||
|
||||
default Predicate<T> and(Predicate<? super T> other) {
|
||||
Objects.requireNonNull(other);
|
||||
return (t) -> test(t) && other.test(t);
|
||||
}
|
||||
|
||||
default Predicate<T> negate() {
|
||||
return (t) -> !test(t);
|
||||
}
|
||||
|
||||
default Predicate<T> or(Predicate<? super T> other) {
|
||||
Objects.requireNonNull(other);
|
||||
return (t) -> test(t) || other.test(t);
|
||||
}
|
||||
|
||||
static <T> Predicate<T> isEqual(Object targetRef) {
|
||||
return (null == targetRef)
|
||||
? Objects::isNull
|
||||
: object -> 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对函数式编程的一个重要应用场景,遍历集合,做了优化,但并没有太多的支持,并且我们强调,不要为了节省代码行数,滥用函数式编程,导致代码可读性变差。
|
||||
|
||||
## 课堂讨论
|
||||
|
||||
你可以说一说函数式编程的优点和缺点,以及你对函数式编程的看法。你觉得它能否替代面向对象编程,成为最主流的编程范式?
|
||||
|
||||
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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 Framework,Spring全家桶中还有更多基于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类->order_info表)。
|
||||
|
||||
实际上,约定优于配置,很好地体现了“二八法则”。在平时的项目开发中,80%的配置使用默认配置就可以了,只有20%的配置必须用户显式地去设置。所以,基于约定来配置,在没有牺牲配置灵活性的前提下,节省了我们大量编写配置的时间,省掉了很多不动脑子的纯体力劳动,提高了开发效率。除此之外,基于相同的约定来做开发,也减少了项目的学习成本和维护成本。
|
||||
|
||||
### 2.低侵入、松耦合
|
||||
|
||||
框架的侵入性是衡量框架好坏的重要指标。所谓低侵入指的是,框架代码很少耦合在业务代码中。低侵入意味着,当我们要替换一个框架的时候,对原有的业务代码改动会很少。相反,如果一个框架是高度侵入的,代码高度侵入到业务代码中,那替换成另一个框架的成本将非常高,甚至几乎不可能。这也是一些长期维护的老项目,使用的框架、技术比较老旧,又无法更新的一个很重要的原因。
|
||||
|
||||
实际上,低侵入是Spring框架遵循的一个非常重要的设计思想。
|
||||
|
||||
Spring提供的IOC容器,在不需要Bean继承任何父类或者实现任何接口的情况下,仅仅通过配置,就能将它们纳入进Spring的管理中。如果我们换一个IOC容器,也只是重新配置一下就可以了,原有的Bean都不需要任何修改。
|
||||
|
||||
除此之外,Spring提供的AOP功能,也体现了低侵入的特性。在项目中,对于非业务功能,比如请求日志、数据采点、安全校验、事务等等,我们没必要将它们侵入进业务代码中。因为一旦侵入,这些代码将分散在各个业务代码中,删除、修改的成本就变得很高。而基于AOP这种开发模式,将非业务代码集中放到切面中,删除、修改的成本就变得很低了。
|
||||
|
||||
### 3.模块化、轻量级
|
||||
|
||||
我们知道,十几年前,EJB是Java企业级应用的主流开发框架。但是,它非常臃肿、复杂,侵入性、耦合性高,开发、维护和学习成本都不低。所以,为了替代笨重的EJB,Rod 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的设计思想,分析一个你熟悉框架、类库、功能组件背后的设计思想。
|
||||
|
||||
欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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<DemoEvent> {
|
||||
@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("null source");
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public Object getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return getClass().getName() + "[source=" + source + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
|
||||
void onApplicationEvent(E var1);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在前面讲到观察者模式的时候,我们提到,观察者需要事先注册到被观察者(JDK的实现方式)或者事件总线(EventBus的实现方式)中。那在Spring的实现中,观察者注册到了哪里呢?又是如何注册的呢?
|
||||
|
||||
我想你应该猜到了,我们把观察者注册到了ApplicationContext对象中。这里的ApplicationContext就相当于Google EventBus框架中的“事件总线”。不过,稍微提醒一下,ApplicationContext这个类并不只是为观察者模式服务的。它底层依赖BeanFactory(IOC的主要实现类),提供应用启动、运行时的上下文信息,是访问这些信息的最顶层接口。
|
||||
|
||||
实际上,具体到源码来说,ApplicationContext只是一个接口,具体的代码实现包含在它的实现类AbstractApplicationContext中。我把跟观察者模式相关的代码,摘抄到了下面。你只需要关注它是如何发送事件和注册监听者就好,其他细节不需要细究。
|
||||
|
||||
```
|
||||
public abstract class AbstractApplicationContext extends ... {
|
||||
private final Set<ApplicationListener<?>> 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<?> listener) {
|
||||
Assert.notNull(listener, "ApplicationListener must not be null");
|
||||
if (this.applicationEventMulticaster != null) {
|
||||
this.applicationEventMulticaster.addApplicationListener(listener);
|
||||
} else {
|
||||
this.applicationListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<ApplicationListener<?>> getApplicationListeners() {
|
||||
return this.applicationListeners;
|
||||
}
|
||||
|
||||
protected void registerListeners() {
|
||||
Iterator var1 = this.getApplicationListeners().iterator();
|
||||
|
||||
while(var1.hasNext()) {
|
||||
ApplicationListener<?> 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 < var3; ++var4) {
|
||||
String listenerBeanName = var7[var4];
|
||||
this.getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
|
||||
}
|
||||
|
||||
Set<ApplicationEvent> 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<?> 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显式地指定初始化方法
|
||||
<bean id="demoBean" class="com.xzg.cd.DemoClass" init-method="initDemo"></bean>
|
||||
|
||||
```
|
||||
|
||||
这种初始化方式有一个缺点,初始化函数并不固定,由用户随意定义,这就需要Spring通过反射,在运行时动态地调用这个初始化函数。而反射又会影响代码执行的性能,那有没有替代方案呢?
|
||||
|
||||
Spring提供了另外一个定义初始化函数的方法,那就是让类实现Initializingbean接口。这个接口包含一个固定的初始化函数定义(afterPropertiesSet()函数)。Spring在初始化Bean的时候,可以直接通过bean.afterPropertiesSet()的方式,调用Bean对象上的这个函数,而不需要使用反射来调用了。我举个例子解释一下,代码如下所示。
|
||||
|
||||
```
|
||||
public class DemoClass implements InitializingBean{
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
//...初始化...
|
||||
}
|
||||
}
|
||||
|
||||
// 配置:不需要显式地指定初始化方法
|
||||
<bean id="demoBean" class="com.xzg.cd.DemoClass"></bean>
|
||||
|
||||
```
|
||||
|
||||
尽管这种实现方式不会用到反射,执行效率提高了,但业务代码(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提供的观察者模式的实现中,是否也支持按照消息类型匹配观察者呢?如果能,它是如何实现的?如果不能,你有什么方法可以让它支持吗?
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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("/employname")
|
||||
public ModelAndView getEmployeeName() {
|
||||
ModelAndView model = new ModelAndView("Greeting");
|
||||
model.addObject("message", "Dinesh");
|
||||
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("Greeting");
|
||||
model.addObject("message", "Dinesh Madhwal");
|
||||
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("Hello World.");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在应用启动的时候,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() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
|
||||
return new JdkDynamicAopProxy(config);
|
||||
} else {
|
||||
Class<?> targetClass = config.getTargetClass();
|
||||
if (targetClass == null) {
|
||||
throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
|
||||
} else {
|
||||
return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//用来判断用哪个动态代理实现方式
|
||||
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
|
||||
Class<?>[] ifcs = config.getProxiedInterfaces();
|
||||
return ifcs.length == 0 || ifcs.length == 1 && 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);
|
||||
<T> T get(Object var1, Class<T> var2);
|
||||
<T> T get(Object var1, Callable<T> 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<?> loader, Throwable ex) {
|
||||
super(String.format("Value for key '%s' could not be loaded using '%s'", 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<String> 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<CacheManager> cacheManagers = new ArrayList();
|
||||
private boolean fallbackToNoOpCache = false;
|
||||
|
||||
public CompositeCacheManager() {
|
||||
}
|
||||
|
||||
public CompositeCacheManager(CacheManager... cacheManagers) {
|
||||
this.setCacheManagers(Arrays.asList(cacheManagers));
|
||||
}
|
||||
|
||||
public void setCacheManagers(Collection<CacheManager> 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<String> getCacheNames() {
|
||||
Set<String> 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, "Target Cache must not be null");
|
||||
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 <T> T get(Object key, Class<T> type) {
|
||||
return this.targetCache.get(key, type);
|
||||
}
|
||||
|
||||
public <T> T get(Object key, Callable<T> 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
|
||||
<bean id="student" class="com.xzg.cd.Student">
|
||||
<constructor-arg name="id" value="1"/>
|
||||
<constructor-arg name="name" value="wangzheng"/>
|
||||
</bean>
|
||||
|
||||
// 使用无参构造函数+setter方法来创建Bean
|
||||
<bean id="student" class="com.xzg.cd.Student">
|
||||
<property name="id" value="1"></property>
|
||||
<property name="name" value="wangzheng"></property>
|
||||
</bean>
|
||||
|
||||
```
|
||||
|
||||
实际上,除了这两种创建Bean的方式之外,我们还可以通过工厂方法来创建Bean。还是刚刚这个例子,用这种方式来创建Bean的话就是下面这个样子:
|
||||
|
||||
```
|
||||
public class StudentFactory {
|
||||
private static Map<Long, Student> students = new HashMap<>();
|
||||
|
||||
static{
|
||||
map.put(1, new Student(1,"wang"));
|
||||
map.put(2, new Student(2,"zheng"));
|
||||
map.put(3, new Student(3,"xzg"));
|
||||
}
|
||||
|
||||
public static Student getStudent(long id){
|
||||
return students.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
// 通过工厂方法getStudent(2)来创建BeanId="zheng""的Bean
|
||||
<bean id="zheng" class="com.xzg.cd.StudentFactory" factory-method="getStudent">
|
||||
<constructor-arg value="2"></constructor-arg>
|
||||
</bean>
|
||||
|
||||
```
|
||||
|
||||
## 其他模式在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,应该如何来编写代码和配置呢?你可以设计一下吗?
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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是一个ORM(Object 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
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="cn.xzg.cd.a87.repo.mapper.UserMapper">
|
||||
<select id="selectById" resultType="cn.xzg.cd.a87.repo.UserDo">
|
||||
select * from user where id=#{id}
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
// 4. 全局配置文件: mybatis.xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE configuration
|
||||
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-config.dtd">
|
||||
<configuration>
|
||||
<environments default="dev">
|
||||
<environment id="dev">
|
||||
<transactionManager type="JDBC"></transactionManager>
|
||||
<dataSource type="POOLED">
|
||||
<property name="driver" value="com.mysql.jdbc.Driver" />
|
||||
<property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8" />
|
||||
<property name="username" value="root" />
|
||||
<property name="password" value="..." />
|
||||
</dataSource>
|
||||
</environment>
|
||||
</environments>
|
||||
<mappers>
|
||||
<mapper resource="mapper/UserMapper.xml"/>
|
||||
</mappers>
|
||||
</configuration>
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,在UserMapper.xml配置文件中,我们只定义了接口和SQL语句之间的映射关系,并没有显式地定义类(UserDo)字段与数据库表(user)字段之间的映射关系。实际上,这就体现了“约定优于配置”的设计原则。类字段与数据库表字段之间使用了默认映射关系:类字段跟数据库表中拼写相同的字段一一映射。当然,如果没办法做到一一映射,我们也可以自定义它们之间的映射关系。
|
||||
|
||||
有了上面的代码和配置,我们就可以像下面这样来访问数据库中的用户信息了。
|
||||
|
||||
```
|
||||
public class MyBatisDemo {
|
||||
public static void main(String[] args) throws IOException {
|
||||
Reader reader = Resources.getResourceAsReader("mybatis.xml");
|
||||
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做背景介绍之外,我们还学习了代码的易用性、性能、灵活性之间的关系。一般来讲,提供的高级功能越多,那性能损耗就会越大些;用起来越简单,提供越简化的开发方式,那灵活性也就相对越低。
|
||||
|
||||
## 课堂讨论
|
||||
|
||||
在你的项目开发中,有没有用过哪些框架,能够切实地提高开发效率,减少不必要的体力劳动?
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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 Interceptor,MyBatis 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 = "query", args = {Statement.class, ResultHandler.class}),
|
||||
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
|
||||
@Signature(type = StatementHandler.class, method = "batch", 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("执行 SQL:[ {} ]执行耗时[ {} ms]", sql, costTime);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
return Plugin.wrap(target, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperties(Properties properties) {
|
||||
System.out.println("插件配置的信息:"+properties);
|
||||
}
|
||||
}
|
||||
|
||||
<!-- MyBatis全局配置文件:mybatis-config.xml -->
|
||||
<plugins>
|
||||
<plugin interceptor="com.xzg.cd.a88.SqlCostTimeInterceptor">
|
||||
<property name="someProperty" value="100"/>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
```
|
||||
|
||||
因为待会我会详细地介绍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("plugins")); //解析插件
|
||||
} catch (Exception e) {
|
||||
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
|
||||
}
|
||||
}
|
||||
|
||||
//解析插件
|
||||
private void pluginElement(XNode parent) throws Exception {
|
||||
if (parent != null) {
|
||||
for (XNode child : parent.getChildren()) {
|
||||
String interceptor = child.getStringAttribute("interceptor");
|
||||
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<Interceptor> interceptors = new ArrayList<Interceptor>();
|
||||
|
||||
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<Interceptor> 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<Class<?>, Set<Method>> signatureMap;
|
||||
|
||||
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
|
||||
this.target = target;
|
||||
this.interceptor = interceptor;
|
||||
this.signatureMap = signatureMap;
|
||||
}
|
||||
|
||||
// wrap()静态方法,用来生成target的动态代理,
|
||||
// 动态代理对象=target对象+interceptor对象。
|
||||
public static Object wrap(Object target, Interceptor interceptor) {
|
||||
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
|
||||
Class<?> type = target.getClass();
|
||||
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
|
||||
if (interfaces.length > 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<Method> methods = signatureMap.get(method.getDeclaringClass());
|
||||
if (methods != null && 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<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
|
||||
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
|
||||
// issue #251
|
||||
if (interceptsAnnotation == null) {
|
||||
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
|
||||
}
|
||||
Signature[] sigs = interceptsAnnotation.value();
|
||||
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
|
||||
for (Signature sig : sigs) {
|
||||
Set<Method> methods = signatureMap.get(sig.type());
|
||||
if (methods == null) {
|
||||
methods = new HashSet<Method>();
|
||||
signatureMap.put(sig.type(), methods);
|
||||
}
|
||||
try {
|
||||
Method method = sig.type().getMethod(sig.method(), sig.args());
|
||||
methods.add(method);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
|
||||
}
|
||||
}
|
||||
return signatureMap;
|
||||
}
|
||||
|
||||
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
|
||||
Set<Class<?>> interfaces = new HashSet<Class<?>>();
|
||||
while (type != null) {
|
||||
for (Class<?> c : type.getInterfaces()) {
|
||||
if (signatureMap.containsKey(c)) {
|
||||
interfaces.add(c);
|
||||
}
|
||||
}
|
||||
type = type.getSuperclass();
|
||||
}
|
||||
return interfaces.toArray(new Class<?>[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那样,提供拦截用户自定义类的方法的功能呢?
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
@@ -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("mybatis.xml");
|
||||
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("Error building SqlSession.", e);
|
||||
} finally {
|
||||
ErrorContext.instance().reset();
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
// Intentionally ignore. Prefer previous error.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SqlSessionFactory build(InputStream inputStream) {
|
||||
return build(inputStream, null, null);
|
||||
}
|
||||
|
||||
public SqlSessionFactory build(InputStream inputStream, String environment) {
|
||||
return build(inputStream, environment, null);
|
||||
}
|
||||
|
||||
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
|
||||
return build(inputStream, null, properties);
|
||||
}
|
||||
|
||||
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
|
||||
try {
|
||||
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
|
||||
return build(parser.parse());
|
||||
} catch (Exception e) {
|
||||
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
|
||||
} finally {
|
||||
ErrorContext.instance().reset();
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
// Intentionally ignore. Prefer previous error.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SqlSessionFactory build(Configuration config) {
|
||||
return new DefaultSqlSessionFactory(config);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
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("Error opening session. Cause: " + 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("Error opening session. Cause: " + e, e);
|
||||
} finally {
|
||||
ErrorContext.instance().reset();
|
||||
}
|
||||
}
|
||||
//...省略部分代码...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从SqlSessionFactory和DefaultSqlSessionFactory的源码来看,它的设计非常类似刚刚讲到的SqlSessionFactoryBuilder,通过重载多个openSession()函数,支持通过组合autoCommit、Executor、Transaction等不同参数,来创建SqlSession对象。标准的工厂模式通过type来创建继承同一个父类的不同子类对象,而这里只不过是通过传递进不同的参数,来创建同一个类的对象。所以,它更像建造者模式。
|
||||
|
||||
虽然设计思路基本一致,但一个叫xxxBuilder(SqlSessionFactoryBuilder),一个叫xxxFactory(SqlSessionFactory)。而且,叫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("executing an update").object(ms.getId());
|
||||
if (closed) {
|
||||
throw new ExecutorException("Executor was closed.");
|
||||
}
|
||||
clearLocalCache();
|
||||
return doUpdate(ms, parameter);
|
||||
}
|
||||
|
||||
public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
|
||||
if (closed) {
|
||||
throw new ExecutorException("Executor was closed.");
|
||||
}
|
||||
return doFlushStatements(isRollBack);
|
||||
}
|
||||
|
||||
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
|
||||
List<E> 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 <E> Cursor<E> 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<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
|
||||
|
||||
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
|
||||
|
||||
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用有待子类实现的抽象方法,那这一般就是模板模式的代码实现。而且,在命名上,模板方法与抽象方法一般是一一对应的,抽象方法在模板方法前面多一个“do”,比如,在BaseExecutor类中,其中一个模板方法叫update(),那对应的抽象方法就叫doUpdate()。
|
||||
|
||||
## SqlNode:如何利用解释器模式来解析动态SQL?
|
||||
|
||||
支持配置文件中编写动态SQL,是MyBatis一个非常强大的功能。所谓动态SQL,就是在SQL中可以包含在trim、if、#{}等语法标签,在运行时根据条件来生成不同的SQL。这么说比较抽象,我举个例子解释一下。
|
||||
|
||||
```
|
||||
<update id="update" parameterType="com.xzg.cd.a89.User"
|
||||
UPDATE user
|
||||
<trim prefix="SET" prefixOverrides=",">
|
||||
<if test="name != null and name != ''">
|
||||
name = #{name}
|
||||
</if>
|
||||
<if test="age != null and age != ''">
|
||||
, age = #{age}
|
||||
</if>
|
||||
<if test="birthday != null and birthday != ''">
|
||||
, birthday = #{birthday}
|
||||
</if>
|
||||
</trim>
|
||||
where id = ${id}
|
||||
</update>
|
||||
|
||||
```
|
||||
|
||||
显然,动态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("line.separator","\n");
|
||||
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
|
||||
|
||||
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<Object, Object> cache = new HashMap<Object, Object>();
|
||||
|
||||
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<Object, Object> keyMap;
|
||||
private Object eldestKey;
|
||||
|
||||
public LruCache(Cache delegate) {
|
||||
this.delegate = delegate;
|
||||
setSize(1024);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return delegate.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return delegate.getSize();
|
||||
}
|
||||
|
||||
public void setSize(final int size) {
|
||||
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
|
||||
private static final long serialVersionUID = 4267176411845948333L;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
|
||||
boolean tooBig = size() > size;
|
||||
if (tooBig) {
|
||||
eldestKey = eldest.getKey();
|
||||
}
|
||||
return tooBig;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putObject(Object key, Object value) {
|
||||
delegate.putObject(key, value);
|
||||
cycleKeyList(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getObject(Object key) {
|
||||
keyMap.get(key); //touch
|
||||
return delegate.getObject(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object removeObject(Object key) {
|
||||
return delegate.removeObject(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
delegate.clear();
|
||||
keyMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadWriteLock getReadWriteLock() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void cycleKeyList(Object key) {
|
||||
keyMap.put(key, key);
|
||||
if (eldestKey != null) {
|
||||
delegate.removeObject(eldestKey);
|
||||
eldestKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
之所以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<PropertyTokenizer> {
|
||||
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 > -1) {
|
||||
name = fullname.substring(0, delim);
|
||||
children = fullname.substring(delim + 1);
|
||||
} else {
|
||||
name = fullname;
|
||||
children = null;
|
||||
}
|
||||
indexedName = name;
|
||||
delim = name.indexOf('[');
|
||||
if (delim > -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("Remove is not supported, as it has no meaning in the context of properties.");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
实际上,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也并非标准的工厂模式。对此你有什么看法呢?
|
||||
|
||||
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
|
||||
Reference in New Issue
Block a user