1/3
2/3
3/3
Dubbo框架大牛 为什么没照搬Joshua Bloch的这个设计模式
AI极客架构师
2022年08月02日 17:00
收录于文集
共7篇

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方法,非常重要。

代码块
clike
自动换行
复制代码
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);
        }
    }
}
复制成功

测试类

代码块
clike
自动换行
复制代码
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());
    }
}
复制成功

JDK base模块中的建造者模式

查询条件:

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类

StringBuilder类,顾名思义,是用来创建String对象的,那为什么不直接创建String对象呢,我相信大部分人都知识,是因为有些情况下,我们需要的字符串,是通过多个步骤,拼接的字符串。当出现“多个步骤”创建对象,建造者模式,就有了用武之地。StringBuilder类也就应运而生了,它里面的append()方法,传入的是需要拼接的字符串,返回的都是它自身,方便用户不断的拼接字符串。“return this”,是建造者模式比较明显的一个特征。但并不是说,具有这个特征的,就一定是建造者模式。

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

方案点评:

从上面的代码可以看出来,StringBuilder类,既符合建造者模式,又具有一定的灵活性,用户不仅可以使用它里面的append()方法,一步一步拼接所需的字符串;而且也可以直接通过构造函数,建立普通的,不需要拼接的字符串,这是它灵活性的一面。

也就是说,对于StringBuilder类,你可以使用它里面的建造者模式,拼接String对象,也可以通过构造函数传入参数,获取相应的String对象。

Calendar类

查询:

repo:^github\.com/openjdk/jdk$ file:^src/java\.base/share/classes/java/util/Calendar\.java

Java类库中的日历工具类,我们的老朋友Calendar类(我们上期分享原型模式时,已经介绍过它了),今天我们看它里面的建造者模式。这个类的高频使用,是众所周知的,为了广大Java程序员能高效简洁的使用它,在JDK1.8中,这个类的开发者们,引入了建造者模式,而且采用了与Joshua Bloch 推荐的建造者模式,完全一致的实现方式,也就是采用了静态内部类的方式。我们先看一下它在JDK1.8前后的使用案例,体验一下它的改进效果,再了解它的源码。

案例对比

代码块
clike
自动换行
复制代码
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类的对象

思考一下自己的项目,看看有没有类似的类,可以考虑升级一下,改善一下用户体验。

Spring框架中的建造者模式

查询语句:

repo:^github\.com/spring-projects/spring-framework$ -file:test lang:Java file:builder.java count:all

从查询结果上看,spring框架中,大约有33个结果,与建造者模式相关,使用率还是非常高的。

UriComponentsBuilder类

repo:^github\.com/spring-projects/spring-framework$ file:^spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder\.java

Spring框架的UriComponentsBuilder是一个非常好的综合性案例,里面既有建造者模式,又有前面我们已经分享的原型模式,还有我们后面要分析的工厂模式,简直是一个创建者模式大全,看来这个模块的架构师,是一个设计模式成瘾的深度患者。看看后面是否有机会,我专门就这个模块,写个专题文章,把这几个模式融会贯通一下。而且对于建造者模式,居然还建立了一个接口UriBuilder。我们先看一个案例,体验一下这个建造者模式。

案例

代码块
clike
自动换行
复制代码
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等

代码块
clike
自动换行
复制代码
@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开头的方法。在这个类的类注释中,介绍了这个类的常见使用方法。

代码块
clike
自动换行
复制代码
/**
 * 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框架中的建造者模式

Dubbo框架,相信大部分Java程序员,对于它都比较熟悉。毕竟现在基于分布式的微服务架构体系大行其道,而Dubbo作为里面的引领者,独占鳌头,风光无限。我们今天就看看它里面的建造者模式。

查询语句

repo:^github\.com/apache/dubbo$ -file:test file:builder.java count:all

dubbo中,涉及建造者模式的类,有50个左右,比spring框架的还多,看来规模还是相当庞大的。

我们挑几个有代表性的类,来看看建造者模式,咱们中国的程序员中,是如何落地的。

上面,我们看了Spring框架中的UriComponentsBuilder类,现在我们看看Dubbo中和它相似的类,看看能不能通过对比,学到点什么,这是spring框架与Dubbo框架,在设计模式上的一次对决,孰强孰弱,我们拭目以待。

URLBuilder类

查询

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,里面定义了四个属性,这个四个属性,模式都一样,都是为了提供效率而增加的,起到缓存的作用,何解?

代码块
clike
自动换行
复制代码
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方法。用户可以根据情况选择合适的方案。

代码块
clike
自动换行
复制代码
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的类层次结构,上面也是一堆父类,确实有些复杂。不过,我们是研究建造者模式的,目标明确,就不会迷路,就容易看懂它的代码及结构。

ServiceBuilder

它里面的建造者模式,和上面的URLBuilder有相似之处,都没有用静态内部类的模式,而是对泛型的使用,到了登峰造极的地步,看来后面我有必要,对泛型,进行一个系列分享,否则看懂这些代码,还是有一定难度的,是否需要,大家可以评论区留言。

代码块
clike
自动换行
复制代码
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中的落地,我们就分享到这里。

后面,《源码说》系列,将分享单例模式,在开源软件中的使用情况。

大家对我的分享方式,或者分享内容,有什么意见或者建议,欢迎评论区留言或者私信我。我会不断进行改进,大家共同进步。

极客架构师,专注架构师成长,我们下期见。