


Spring框架与阿里Dubbo框架的一次巅峰对决,同样的组件,使用同样的设计模式,Spring框架大牛与Dubbo框架大牛,谁是牛中之牛;你想知道dubbo框架的大牛,为何没使用标准的建造者模式,它们两者有何区别,哪个更合适;Spring框架的这个组件,竟然应用了三种设计模式,估计是设计模式深度患者;JDK中哪些类,应用了建造者模式。

谁家的牛?
大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。
本期是极客架构师推出的《源码说》的第4期。
在第3期,我给大家分享了,原型模式是如何在JDK的ArrayList,HashMap中实现的,以及在Spring框架及阿里的FastJson类库中原型模式。
在本期,我将给大家分享,建造者模式,在JDK,spring框架,Dubbo框架中的落地方案,看看不同的框架,在建造者模式落地时,都进行了哪些调整优化,还有就是通用的组件,spring框架与dubbo框架,两家的建造者模式,孰强孰弱。


建造者模式要点回顾
JDK base模块中的建造者模式
StringBuilder类
Calendar类
Spring框架中的建造者模式
UriComponentsBuilder类
Dubbo框架中的建造者模式
URLBuilder类
ServiceBuilder类
有关建造者模式的完整文章和视频,没有看过的朋友,可以到我的文集和视频集中查看,有助于帮你理解后面的内容。
我们只回顾建造者模式的重点内容。
落地方案:Joshua Bloch改进的建造者模式(主流方式)
下面这个实现了建造者模式的类,注意以下6点:
1,它里面的静态内部类MyCacheBuilder才是神来之笔。(主类是产品,静态内部类是工厂,工厂生产产品)
2,它的构造函数是private的(有点像单例模式,防止用户绕开工厂,自己直接建立对象)
3,它的参数,没有提供set方法(主类(产品)是只读的,绝对的线程安全)
4,它里面必须(不能为空)的参数,添加了final关键字
5,静态内部类MyCacheBuilder的set方法,不同于常规的set方法,注意它的返回值
6,静态内部类MyCacheBuilder的build方法,非常重要。
package com.geekarchitect.patterns.builder.demo04;
import java.util.Map;
/**
* @author 极客架构师@吴念
* @createTime 2022/7/6
*/
public class MyCache<K, V> {
/**
* 初始化容量,必须
*/
private final int initialCapacity;
/**
* 最大数量,必须
*/
private final long maximumSize;
/**
* 并行等级。决定segment数量的参数
*/
private int concurrencyLevel = -1;
/**
* 最大权重
*/
private long maximumWeight = -1L;
/**
* 写操作后失效时间
*/
private long expireAfterWriteNanos = -1L;
/**
* 访问操作后失效时间
*/
private long expireAfterAccessNanos = -1L;
private MyCache(MyCacheBuilder myCacheBuilder) {
this.initialCapacity = myCacheBuilder.initialCapacity;
this.maximumSize = myCacheBuilder.maximumSize;
this.concurrencyLevel = myCacheBuilder.concurrencyLevel;
this.maximumWeight = myCacheBuilder.maximumWeight;
this.expireAfterWriteNanos = myCacheBuilder.expireAfterWriteNanos;
this.expireAfterAccessNanos = myCacheBuilder.expireAfterAccessNanos;
}
@Override
public String toString() {
return "MyCache{" +
"initialCapacity=" + initialCapacity +
", maximumSize=" + maximumSize +
", concurrencyLevel=" + concurrencyLevel +
", maximumWeight=" + maximumWeight +
", expireAfterWriteNanos=" + expireAfterWriteNanos +
", expireAfterAccessNanos=" + expireAfterAccessNanos +
'}';
}
public void put(K key, V value) {
}
public V get(K key) {
return null;
}
public static class MyCacheBuilder<K, V> {
/**
* 初始化容量,必须
*/
private final int initialCapacity;
/**
* 最大数量,必须
*/
private final long maximumSize;
private final Map<String, String> cacheMap = null;
/**
* 并行等级。决定segment数量的参数
*/
private int concurrencyLevel = -1;
/**
* 最大权重
*/
private long maximumWeight = -1L;
/**
* 写操作后失效时间
*/
private long expireAfterWriteNanos = -1L;
/**
* 访问操作后失效时间
*/
private long expireAfterAccessNanos = -1L;
public MyCacheBuilder(int initialCapacity, long maximumSize) {
this.initialCapacity = initialCapacity;
this.maximumSize = maximumSize;
}
public MyCacheBuilder setConcurrencyLevel(int concurrencyLevel) {
this.concurrencyLevel = concurrencyLevel;
return this;
}
public MyCacheBuilder setMaximumWeight(long maximumWeight) {
this.maximumWeight = maximumWeight;
return this;
}
public MyCacheBuilder setExpireAfterWriteNanos(long expireAfterWriteNanos) {
this.expireAfterWriteNanos = expireAfterWriteNanos;
return this;
}
public MyCacheBuilder setExpireAfterAccessNanos(long expireAfterAccessNanos) {
this.expireAfterAccessNanos = expireAfterAccessNanos;
return this;
}
public MyCache build() {
return new MyCache<K, V>(this);
}
}
}
测试类
package com.geekarchitect.patterns.builder.demo04;
import com.geekarchitect.patterns.builder.demo03.TestCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author 极客架构师@吴念
* @createTime 2022/7/6
*/
public class TestMyCache {
private static final Logger LOG = LoggerFactory.getLogger(TestMyCache.class);
public static void main(String[] args) {
TestMyCache testMyCache=new TestMyCache();
testMyCache.demo01();
}
public void demo01() {
MyCache<String, String> myCache = new MyCache.MyCacheBuilder(10, 100L)
.setConcurrencyLevel(20)
.setMaximumWeight(30L).build();
LOG.info(myCache.toString());
}
}
查询条件:
repo:^github\.com/openjdk/jdk$ file:^src/java\.base -file:test lang:Java file:builder.java count:all
注意:
建造者模式,一般类名以builder结尾,但是也有特殊情况,我们前面举的案例,就是内部类以builder结尾,按照我们现在的查询方式,应该是查询不出来,但是现有的案例,已经足够我们研究了。
我们仍然是查询JDK基础模块里面的类,而且是JDK的最新版本,关于JDK的查询范围和版本问题,我们在上期,专门说明了一下。以便我们找的案例,都是大家相对比较熟悉的类。

从查询结果看,JDK基础模块里面,符合我们查询条件的,与建造者模式有关键的结果,有14个,足够我们研究了。
我们先看第一个,也是大家非常熟悉的,面试常常被问的类,StringBuilder类。
StringBuilder类,顾名思义,是用来创建String对象的,那为什么不直接创建String对象呢,我相信大部分人都知识,是因为有些情况下,我们需要的字符串,是通过多个步骤,拼接的字符串。当出现“多个步骤”创建对象,建造者模式,就有了用武之地。StringBuilder类也就应运而生了,它里面的append()方法,传入的是需要拼接的字符串,返回的都是它自身,方便用户不断的拼接字符串。“return this”,是建造者模式比较明显的一个特征。但并不是说,具有这个特征的,就一定是建造者模式。

下面截图,是StringBuilder类的构造函数,注意这些函数,它们是违反建造者模式的,但是却体现了这个类的灵活之处。

方案点评:
从上面的代码可以看出来,StringBuilder类,既符合建造者模式,又具有一定的灵活性,用户不仅可以使用它里面的append()方法,一步一步拼接所需的字符串;而且也可以直接通过构造函数,建立普通的,不需要拼接的字符串,这是它灵活性的一面。
也就是说,对于StringBuilder类,你可以使用它里面的建造者模式,拼接String对象,也可以通过构造函数传入参数,获取相应的String对象。
查询:
repo:^github\.com/openjdk/jdk$ file:^src/java\.base/share/classes/java/util/Calendar\.java
Java类库中的日历工具类,我们的老朋友Calendar类(我们上期分享原型模式时,已经介绍过它了),今天我们看它里面的建造者模式。这个类的高频使用,是众所周知的,为了广大Java程序员能高效简洁的使用它,在JDK1.8中,这个类的开发者们,引入了建造者模式,而且采用了与Joshua Bloch 推荐的建造者模式,完全一致的实现方式,也就是采用了静态内部类的方式。我们先看一下它在JDK1.8前后的使用案例,体验一下它的改进效果,再了解它的源码。
案例对比
package com.geekarchitect.patterns.builder.demo05;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import static java.util.Calendar.*;
/**
* @author 极客架构师@吴念
* @createTime 2022/7/25
*/
public class TestCalendar {
private static final Logger LOG = LoggerFactory.getLogger(TestCalendar.class);
public static void main(String[] args) {
TestCalendar testCalendar = new TestCalendar();
testCalendar.demo01();
testCalendar.demo02();
}
public void demo01() {
Calendar calendar =
Calendar.getInstance();
calendar.set(YEAR, 2022);
calendar.set(MONTH, JULY);
calendar.set(DATE, 7);
calendar.set(HOUR, 1);
calendar.set(MINUTE, 5);
calendar.set(SECOND, 10);
calendar.set(AM_PM, PM);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
LOG.info("calendar传统用法案例 {}", sdf.format(calendar.getTime()));
}
public void demo02() {
Calendar calendar = new Calendar.Builder()
.set(YEAR, 2022)
.set(MONTH, JULY)
.set(DATE, 7)
.set(HOUR, 1)
.set(MINUTE, 5)
.set(SECOND, 10)
.set(AM_PM, PM).build();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
LOG.info("calendar JDK1.8 新用法案例 {}", sdf.format(calendar.getTime()));
}
}

静态内部类Builder
在JDK1.8,Calendar类,新增了一个静态内部类,实现了建造者模式。

静态内部类中的set()方法
注意它的返回值。

静态内部类的build()方法
它的返回值是Calendar类的对象

思考一下自己的项目,看看有没有类似的类,可以考虑升级一下,改善一下用户体验。
查询语句:
repo:^github\.com/spring-projects/spring-framework$ -file:test lang:Java file:builder.java count:all

从查询结果上看,spring框架中,大约有33个结果,与建造者模式相关,使用率还是非常高的。
repo:^github\.com/spring-projects/spring-framework$ file:^spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder\.java
Spring框架的UriComponentsBuilder是一个非常好的综合性案例,里面既有建造者模式,又有前面我们已经分享的原型模式,还有我们后面要分析的工厂模式,简直是一个创建者模式大全,看来这个模块的架构师,是一个设计模式成瘾的深度患者。看看后面是否有机会,我专门就这个模块,写个专题文章,把这几个模式融会贯通一下。而且对于建造者模式,居然还建立了一个接口UriBuilder。我们先看一个案例,体验一下这个建造者模式。

案例
package com.geekarchitect.patterns.builder.demo05;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author 极客架构师@吴念
* @createTime 2022/7/26
*/
public class TestUriComponentsBuilder {
private static final Logger LOG = LoggerFactory.getLogger(TestUriComponentsBuilder.class);
public static void main(String[] args) {
TestUriComponentsBuilder testUriComponentsBuilder=new TestUriComponentsBuilder();
testUriComponentsBuilder.demo01();
}
public void demo01(){
UriComponents uriComponents = UriComponentsBuilder.newInstance()
.scheme("https")
.host("space.bilibili.com")
.path("//1135139396/search/video")
.queryParam("keyword", "设计模式")
.build();
LOG.info("UriComponentsBuilder 案例");
LOG.info("URL = {}",uriComponents.toUri());
}
}
运行结果

从上面的代码看,UriComponentsBuilder是用来建立UriComponents类的对象。
UriComponents类
它是一个抽象类,执行了Serializable接口,说明它是一个支持序列化的类。它里面定义了组成Uri的各个部分,如:scheme,Host,path等
@SuppressWarnings("serial")
public abstract class UriComponents implements Serializable {
/** Captures URI template variable names. */
private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
@Nullable
private final String scheme;
@Nullable
private final String fragment;
protected UriComponents(@Nullable String scheme, @Nullable String fragment) {
this.scheme = scheme;
this.fragment = fragment;
}
// Component getters
/**
* Return the scheme. Can be {@code null}.
*/
@Nullable
public final String getScheme() {
return this.scheme;
}
/**
* Return the fragment. Can be {@code null}.
*/
@Nullable
public final String getFragment() {
return this.fragment;
}
/**
* Return the scheme specific part. Can be {@code null}.
*/
@Nullable
public abstract String getSchemeSpecificPart();
/**
* Return the user info. Can be {@code null}.
*/
@Nullable
public abstract String getUserInfo();
/**
* Return the host. Can be {@code null}.
*/
@Nullable
public abstract String getHost();
/**
* Return the port. {@code -1} if no port has been set.
*/
public abstract int getPort();
/**
* Return the path. Can be {@code null}.
*/
@Nullable
public abstract String getPath();
/**
* Return the list of path segments. Empty if no path has been set.
*/
public abstract List<String> getPathSegments();
/**
* Return the query. Can be {@code null}.
*/
@Nullable
public abstract String getQuery();
UriComponentsBuilder类,执行了UriBuilder接口,Builder模式,一般很少有接口。这个接口,不仅引出了建造者模式,里面还有工厂模式。
UriBuilder接口

UriComponentsBuilder

UriComponentsBuilder
下面代码,我截取了这个类的一部分方法,以from开头的方法。在这个类的类注释中,介绍了这个类的常见使用方法。
/**
* Builder for {@link UriComponents}.
*
* <p>Typical usage involves:
* <ol>
* <li>Create a {@code UriComponentsBuilder} with one of the static factory methods
* (such as {@link #fromPath(String)} or {@link #fromUri(URI)})</li>
* <li>Set the various URI components through the respective methods ({@link #scheme(String)},
* {@link #userInfo(String)}, {@link #host(String)}, {@link #port(int)}, {@link #path(String)},
* {@link #pathSegment(String...)}, {@link #queryParam(String, Object...)}, and
* {@link #fragment(String)}.</li>
* <li>Build the {@link UriComponents} instance with the {@link #build()} method.</li>
* </ol>
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Phillip Webb
* @author Oliver Gierke
* @author Brian Clozel
* @author Sebastien Deleuze
* @author Sam Brannen
* @since 3.1
* @see #newInstance()
* @see #fromPath(String)
* @see #fromUri(URI)
*/
public class UriComponentsBuilder implements UriBuilder, Cloneable {
/**
* Create a builder that is initialized with the given path.
* @param path the path to initialize with
* @return the new {@code UriComponentsBuilder}
*/
public static UriComponentsBuilder fromPath(String path) {
UriComponentsBuilder builder = new UriComponentsBuilder();
builder.path(path);
return builder;
}
/**
* Create a builder that is initialized from the given {@code URI}.
* <p><strong>Note:</strong> the components in the resulting builder will be
* in fully encoded (raw) form and further changes must also supply values
* that are fully encoded, for example via methods in {@link UriUtils}.
* In addition please use {@link #build(boolean)} with a value of "true" to
* build the {@link UriComponents} instance in order to indicate that the
* components are encoded.
* @param uri the URI to initialize with
* @return the new {@code UriComponentsBuilder}
*/
public static UriComponentsBuilder fromUri(URI uri) {
UriComponentsBuilder builder = new UriComponentsBuilder();
builder.uri(uri);
return builder;
}
/**
* Create a builder that is initialized with the given URI string.
* <p><strong>Note:</strong> The presence of reserved characters can prevent
* correct parsing of the URI string. For example if a query parameter
* contains {@code '='} or {@code '&'} characters, the query string cannot
* be parsed unambiguously. Such values should be substituted for URI
* variables to enable correct parsing:
* <pre class="code">
* String uriString = "/hotels/42?filter={value}";
* UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold");
* </pre>
* @param uri the URI string to initialize with
* @return the new {@code UriComponentsBuilder}
*/
public static UriComponentsBuilder fromUriString(String uri) {
Assert.notNull(uri, "URI must not be null");
Matcher matcher = URI_PATTERN.matcher(uri);
if (matcher.matches()) {
UriComponentsBuilder builder = new UriComponentsBuilder();
String scheme = matcher.group(2);
String userInfo = matcher.group(5);
String host = matcher.group(6);
String port = matcher.group(8);
String path = matcher.group(9);
String query = matcher.group(11);
String fragment = matcher.group(13);
boolean opaque = false;
if (StringUtils.hasLength(scheme)) {
String rest = uri.substring(scheme.length());
if (!rest.startsWith(":/")) {
opaque = true;
}
}
builder.scheme(scheme);
if (opaque) {
String ssp = uri.substring(scheme.length() + 1);
if (StringUtils.hasLength(fragment)) {
ssp = ssp.substring(0, ssp.length() - (fragment.length() + 1));
}
builder.schemeSpecificPart(ssp);
}
else {
if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) {
throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL");
}
builder.userInfo(userInfo);
builder.host(host);
if (StringUtils.hasLength(port)) {
builder.port(port);
}
builder.path(path);
builder.query(query);
}
if (StringUtils.hasText(fragment)) {
builder.fragment(fragment);
}
return builder;
}
else {
throw new IllegalArgumentException("[" + uri + "] is not a valid URI");
}
}
/**
* Create a URI components builder from the given HTTP URL String.
* <p><strong>Note:</strong> The presence of reserved characters can prevent
* correct parsing of the URI string. For example if a query parameter
* contains {@code '='} or {@code '&'} characters, the query string cannot
* be parsed unambiguously. Such values should be substituted for URI
* variables to enable correct parsing:
* <pre class="code">
* String urlString = "https://example.com/hotels/42?filter={value}";
* UriComponentsBuilder.fromHttpUrl(urlString).buildAndExpand("hot&cold");
* </pre>
* @param httpUrl the source URI
* @return the URI components of the URI
*/
public static UriComponentsBuilder fromHttpUrl(String httpUrl) {
Assert.notNull(httpUrl, "HTTP URL must not be null");
Matcher matcher = HTTP_URL_PATTERN.matcher(httpUrl);
if (matcher.matches()) {
UriComponentsBuilder builder = new UriComponentsBuilder();
String scheme = matcher.group(1);
builder.scheme(scheme != null ? scheme.toLowerCase() : null);
builder.userInfo(matcher.group(4));
String host = matcher.group(5);
if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) {
throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
}
builder.host(host);
String port = matcher.group(7);
if (StringUtils.hasLength(port)) {
builder.port(port);
}
builder.path(matcher.group(8));
builder.query(matcher.group(10));
String fragment = matcher.group(12);
if (StringUtils.hasText(fragment)) {
builder.fragment(fragment);
}
return builder;
}
else {
throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
}
}
/**
* Create a new {@code UriComponents} object from the URI associated with
* the given HttpRequest while also overlaying with values from the headers
* "Forwarded" (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>),
* or "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if
* "Forwarded" is not found.
* @param request the source request
* @return the URI components of the URI
* @since 4.1.5
* @see #parseForwardedFor(HttpRequest, InetSocketAddress)
*/
public static UriComponentsBuilder fromHttpRequest(HttpRequest request) {
return fromUri(request.getURI()).adaptFromForwardedHeaders(request.getHeaders());
}
}
从UriComponentsBuilder这个类的类注释中,我们可以看到,这个类推荐的使用方法是,先采用from开头的这些方法(这些方法,就是工厂系列里面的临时工,简单工厂模式,也叫静态工厂模式),创建出Builder对象,然后根据情况,调用相应的set方法即可。这个思路,可以大大提高程序员,使用这个类的效率,在后面Dubbo的URLBuilder类中,也出现了类似的解决方案。这个我认为,是对建造者模式的一个重大改进。
和上期一样,咱们不能只看国外的代码, 也看看咱们中国程序员写的代码,对码农加强一下爱国主义教育,提升一下民族自信心。
这次我们看看大家比较熟悉的阿里Dubbo框架,它里面大量使用了建造者模式,代码非常具有参考价值。
Dubbo框架,相信大部分Java程序员,对于它都比较熟悉。毕竟现在基于分布式的微服务架构体系大行其道,而Dubbo作为里面的引领者,独占鳌头,风光无限。我们今天就看看它里面的建造者模式。
查询语句
repo:^github\.com/apache/dubbo$ -file:test file:builder.java count:all

dubbo中,涉及建造者模式的类,有50个左右,比spring框架的还多,看来规模还是相当庞大的。
我们挑几个有代表性的类,来看看建造者模式,咱们中国的程序员中,是如何落地的。
上面,我们看了Spring框架中的UriComponentsBuilder类,现在我们看看Dubbo中和它相似的类,看看能不能通过对比,学到点什么,这是spring框架与Dubbo框架,在设计模式上的一次对决,孰强孰弱,我们拭目以待。

查询
repo:^github\.com/apache/dubbo$ file:^dubbo-common/src/main/java/org/apache/dubbo/common/URLBuilder\.java

继承关系
如下图,URLBuilder 继承 ServiceConfigURL,ServiceConfigURL 继承 URL,注意,这个URL并不是JDK里面的java.net.URL(这个类是final的,不能继承),而是org.apache.dubbo.common.URL.看来dubbo框架的开发者很嚣张啊,JDK原有的URL都无法满足他们的需求啦,需要他们自己扩展自己的URL类了。这种基础性的类,在普通的项目里面,一般是不敢轻易自定义的,因为牵一发而动全身,很多地方都得进行扩展。

org.apache.dubbo.common.URL
这个类的注释里面对url各个部分的讲解,特别是第二部分,特殊Url案例,讲解的非常详细,大家这方面知识薄弱的,正好可以补充一下。

ServiceConfigURL
这个类继承了URL,里面定义了四个属性,这个四个属性,模式都一样,都是为了提供效率而增加的,起到缓存的作用,何解?
public class ServiceConfigURL extends URL {
private volatile transient String full;
private volatile transient String string;
private volatile transient String identity;
private volatile transient String parameter;
public ServiceConfigURL() {
super();
}
public ServiceConfigURL(URLAddress urlAddress, URLParam urlParam, Map<String, Object> attributes) {
super(urlAddress, urlParam, attributes);
}
@Override
public String toString() {
if (string != null) {
return string;
}
return string = super.toString();
}
@Override
public String toFullString() {
if (full != null) {
return full;
}
return full = super.toFullString();
}
@Override
public String toIdentityString() {
if (identity != null) {
return identity;
}
return identity = super.toIdentityString();
}
@Override
public String toParameterString() {
if (parameter != null) {
return parameter;
}
return parameter = super.toParameterString();
}
}
我以full属性为例给大家讲解,大家就可以触类旁通了。
这个full属性,被用在如下的toFullString()方法里面,这种代码是典型的查询缓存的代码模式。说明super.toFullString()应该是一个重量级操作。需要对它的结果,进行缓存,以便提升效率。

URL的toFullString()
如下所示,URL里面的toFullString()依赖的是buildString()方法,而这个方法里面,一大堆的业务逻辑需要处理,所以是一个重量级的操作。所以,非常有必要对它的结果进行缓存,避免造成资源浪费。


full属性的volatile和transient修饰符
transient:我们在前面刚刚分享过,表示该属性不支持序列化。因为它是缓存值,所以,没有缓存的必要,被反序列化之后为空,可以再重新生成新的值。
volatile:精通多线程的朋友,应该清楚它的含义,表示所修饰的属性,在多线程环境下的可见性。这个说起来话长,我这次就不进行扩展了,后面我会推出《极客基本功之并发编程》,会对这个修饰符,进行系统阐述。
我们言归正传,回到建造者设计模式上面,看看URLBuilder实现的建造者模式,有哪些值得我们学习的。
建造者模式点评
1,大胆使用重叠式构造函数
我在分享建造者模式时,对重叠式构造函数的解决方案,进行了详细讲解,并且强调了它的弊端,但是如果把它和建造者模式,同时使用,可以大大方便用户的使用。如果单纯只使用建造者模式,每个参数,都需要用户调用相应的set方法进行初始化,虽然使用了链式调用,但是代码仍然有些繁琐,如果对于用户经常联合使用的参数,采用构造函数进行初始化,可以大大简化用户的代码。
而URLBuilder类,就是很好的证明。看看下面的代码,它定义了一系列的构造函数。同时也定义了相应的set方法。用户可以根据情况选择合适的方案。
public final class URLBuilder extends ServiceConfigURL {
private String protocol;
private String username;
private String password;
// by default, host to registry
private String host;
// by default, port to registry
private int port;
private String path;
private Map<String, String> parameters;
private Map<String, Object> attributes;
private Map<String, Map<String, String>> methodParameters;
public URLBuilder() {
protocol = null;
username = null;
password = null;
host = null;
port = 0;
path = null;
parameters = new HashMap<>();
attributes = new HashMap<>();
methodParameters = new HashMap<>();
}
public URLBuilder(String protocol, String host, int port) {
this(protocol, null, null, host, port, null, null);
}
public URLBuilder(String protocol, String host, int port, String[] pairs) {
this(protocol, null, null, host, port, null, CollectionUtils.toStringMap(pairs));
}
public URLBuilder(String protocol, String host, int port, Map<String, String> parameters) {
this(protocol, null, null, host, port, null, parameters);
}
public URLBuilder(String protocol, String host, int port, String path) {
this(protocol, null, null, host, port, path, null);
}
public URLBuilder(String protocol, String host, int port, String path, String... pairs) {
this(protocol, null, null, host, port, path, CollectionUtils.toStringMap(pairs));
}
public URLBuilder(String protocol, String host, int port, String path, Map<String, String> parameters) {
this(protocol, null, null, host, port, path, parameters);
}
public URLBuilder(String protocol,
String username,
String password,
String host,
int port,
String path,
Map<String, String> parameters) {
this(protocol, username, password, host, port, path, parameters, null);
}
public URLBuilder(String protocol,
String username,
String password,
String host,
int port,
String path,
Map<String, String> parameters,
Map<String, Object> attributes) {
this.protocol = protocol;
this.username = username;
this.password = password;
this.host = host;
this.port = port;
this.path = path;
this.parameters = parameters != null ? parameters : new HashMap<>();
this.attributes = attributes != null ? attributes : new HashMap<>();
}
2,没有采用静态内部类模式
我认为,对于建造者模式,Joshua Bloch的静态内部类机制,是一种比较好的落地方案,在多线程环境下,会更安全,但是URLBuilder并没有使用这个方案,我分析有一个比较大的原因,就是这个类,并不是给普通用户使用的,而是框架内部自用的,所以这个类,并没有考虑的那么严格,正常使用,一般是不会有线程安全问题的。
3,根据内容反向生产对象(静态工厂方法模式)
URLBuilder类里面,还有一个非常值得我们学习的点,就是根据URL字符串,反向生成URLBuilder对象。我们生成URLBuilder对象,目的是为了对URL各个部分进行解析,以便我们使用URL,而URL本身,又可以表示为常见的URL字符串,如:
“http://www.facebook.com/friends?param1=value1&param2=value2”
那么根据URL字符串,反向生成对应的对象,也可以大大提高用户创建对象的效率,因为用户对URL地址,还是比较熟悉的。URLBuilder类里面的from方法,实现了静态工厂方法,就起到这个作用。

URLBuilder案例
Dubbo项目里面,关于URLBuilder的单元测试类,对URLBuilder类的使用,有很好的案例,特别是对from()方法的使用,可以说是淋漓尽致。
repo:^github\.com/apache/dubbo$ file:^dubbo-common/src/test/java/org/apache/dubbo/common/URLBuilderTest\.java

URLBuilder类里面的建造者模式,我们就讲到这里,它与Spring框架的UriComponentsBuilder类,孰强孰弱,大家自己评判,我就不剥夺大家思考的空间了。
熟悉dubbo框架的朋友,对下面这张图里面的组件,应该不会陌生吧,Dubbo作为微服务框架,框架使用者最常用的两类组件,应该是Service(服务提供者)和Reference(服务消费者)这两个组件吧,我们就以Service为例进行说明。
dubbo中,对组件的配置,可以通过各种途径,xml方式,API方式,注解方式等等,无论哪种方式,对于Service组件,用户的配置信息,都要通过ServiceConfig对象来封装,而ServiceConfig则可以通过ServiceBuilder类来生成,这个类就是我们研究的对象,因为它里面使用了建造者模式。

ServiceConfig
对于一个建造者模式,我们在研究它之前,好好的看看它所生成的产品,或者叫对象。对我们理解建造者模式,有很大的帮助。

上面是ServiceConfig类,这个可能不直观,看下面的类图,更直观一些。
可以看出来,ServiceConfig上面继承了多层,每个父类,都定义了一些属性,比较重要的我已经标出来了。但是,不论再复杂,它也是封装数据用的,相当于实体类,业务逻辑不会太复杂。
看来,dubbo框架的类层次结构,还是比较深的。

下面我们看看,用于建立ServiceConfig对象的ServiceBuilder类,它里面实现了建造者模式。
下图是ServiceBuilder的类层次结构,上面也是一堆父类,确实有些复杂。不过,我们是研究建造者模式的,目标明确,就不会迷路,就容易看懂它的代码及结构。

ServiceBuilder
它里面的建造者模式,和上面的URLBuilder有相似之处,都没有用静态内部类的模式,而是对泛型的使用,到了登峰造极的地步,看来后面我有必要,对泛型,进行一个系列分享,否则看懂这些代码,还是有一定难度的,是否需要,大家可以评论区留言。
public class ServiceBuilder<U> extends AbstractServiceBuilder<ServiceConfig<U>, ServiceBuilder<U>> {
/**
* The interface name of the exported service
*/
private String interfaceName;
/**
* The interface class of the exported service
*/
private Class<?> interfaceClass;
/**
* The reference of the interface implementation
*/
private U ref;
/**
* The service name
*/
private String path;
/**
* The method configuration
*/
private List<MethodConfig> methods;
/**
* The provider configuration
*/
private ProviderConfig provider;
/**
* The providerIds
*/
private String providerIds;
/**
* whether it is a GenericService
*/
private String generic;
public static <T> ServiceBuilder<T> newBuilder() {
return new ServiceBuilder<>();
}
public ServiceBuilder<U> id(String id) {
return super.id(id);
}
public ServiceBuilder<U> interfaceName(String interfaceName) {
this.interfaceName = interfaceName;
return getThis();
}
public ServiceBuilder<U> interfaceClass(Class<?> interfaceClass) {
this.interfaceClass = interfaceClass;
return getThis();
}
public ServiceBuilder<U> ref(U ref) {
this.ref = ref;
return getThis();
}
public ServiceBuilder<U> path(String path) {
this.path = path;
return getThis();
}
public ServiceBuilder<U> addMethod(MethodConfig method) {
if (this.methods == null) {
this.methods = new ArrayList<>();
}
this.methods.add(method);
return getThis();
}
public ServiceBuilder<U> addMethods(List<? extends MethodConfig> methods) {
if (this.methods == null) {
this.methods = new ArrayList<>();
}
this.methods.addAll(methods);
return getThis();
}
public ServiceBuilder<U> provider(ProviderConfig provider) {
this.provider = provider;
return getThis();
}
public ServiceBuilder<U> providerIds(String providerIds) {
this.providerIds = providerIds;
return getThis();
}
public ServiceBuilder<U> generic(String generic) {
this.generic = generic;
return getThis();
}
public ServiceConfig<U> build() {
ServiceConfig<U> serviceConfig = new ServiceConfig<>();
super.build(serviceConfig);
serviceConfig.setInterface(interfaceName);
serviceConfig.setInterface(interfaceClass);
serviceConfig.setRef(ref);
serviceConfig.setPath(path);
serviceConfig.setMethods(methods);
serviceConfig.setProvider(provider);
serviceConfig.setProviderIds(providerIds);
serviceConfig.setGeneric(generic);
return serviceConfig;
}
@Override
protected ServiceBuilder<U> getThis() {
return this;
}
}
案例:

小结一下,对于Dubbo框架中的建造者模式,仅从URLBuilder类和ServiceBuilder类来看,它们确实有不少相似之处,都没有采用标准的,Joshua Bloch 推荐的建造者模式。也就是没有采用静态内部类的方式。创建的对象,也不是只读的,因为只读对象,具有很高的线程安全性,我估计是因为这些类,都是框架内部自用的,所以只考虑了用户正常使用的情况,而不会出现线程安全问题。另外,对泛型技术使用的比较熟练,看来进阿里大厂,不精通泛型是不行的。
至此,关于建造者模式,在开源软件JDK,Spring,Dubbo中的落地,我们就分享到这里。
后面,《源码说》系列,将分享单例模式,在开源软件中的使用情况。
大家对我的分享方式,或者分享内容,有什么意见或者建议,欢迎评论区留言或者私信我。我会不断进行改进,大家共同进步。
极客架构师,专注架构师成长,我们下期见。