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 { /** * 初始化容量,必须 */ 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 { /** * 初始化容量,必须 */ private final int initialCapacity; /** * 最大数量,必须 */ private final long maximumSize; private final Map 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(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 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 getPathSegments(); /** * Return the query. Can be {@code null}. */ @Nullable public abstract String getQuery();
UriComponentsBuilder类,执行了UriBuilder接口,Builder模式,一般很少有接口。这个接口,不仅引出了建造者模式,里面还有工厂模式。
UriBuilder接口
UriComponentsBuilder
UriComponentsBuilder
下面代码,我截取了这个类的一部分方法,以from开头的方法。在这个类的类注释中,介绍了这个类的常见使用方法。
/** * Builder for {@link UriComponents}. * * Typical usage involves: *
* - Create a {@code UriComponentsBuilder} with one of the static factory methods * (such as {@link #fromPath(String)} or {@link #fromUri(URI)})
* - 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)}.
* - Build the {@link UriComponents} instance with the {@link #build()} method.
*
* * @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}. * Note: 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. *
Note: 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: *
* String uriString = "/hotels/42?filter={value}"; * UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold"); *
* @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. * Note: 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: *
* String urlString = "https://example.com/hotels/42?filter={value}"; * UriComponentsBuilder.fromHttpUrl(urlString).buildAndExpand("hot&cold"); *
* @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" (RFC 7239), * 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 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 parameters; private Map attributes; private Map> 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 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 parameters) { this(protocol, null, null, host, port, path, parameters); } public URLBuilder(String protocol, String username, String password, String host, int port, String path, Map 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 parameters, Map 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¶m2=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 extends AbstractServiceBuilder, ServiceBuilder> { /** * 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 methods; /** * The provider configuration */ private ProviderConfig provider; /** * The providerIds */ private String providerIds; /** * whether it is a GenericService */ private String generic; public static ServiceBuilder newBuilder() { return new ServiceBuilder<>(); } public ServiceBuilder id(String id) { return super.id(id); } public ServiceBuilder interfaceName(String interfaceName) { this.interfaceName = interfaceName; return getThis(); } public ServiceBuilder interfaceClass(Class<?> interfaceClass) { this.interfaceClass = interfaceClass; return getThis(); } public ServiceBuilder ref(U ref) { this.ref = ref; return getThis(); } public ServiceBuilder path(String path) { this.path = path; return getThis(); } public ServiceBuilder addMethod(MethodConfig method) { if (this.methods == null) { this.methods = new ArrayList<>(); } this.methods.add(method); return getThis(); } public ServiceBuilder addMethods(List<? extends MethodConfig> methods) { if (this.methods == null) { this.methods = new ArrayList<>(); } this.methods.addAll(methods); return getThis(); } public ServiceBuilder provider(ProviderConfig provider) { this.provider = provider; return getThis(); } public ServiceBuilder providerIds(String providerIds) { this.providerIds = providerIds; return getThis(); } public ServiceBuilder generic(String generic) { this.generic = generic; return getThis(); } public ServiceConfig build() { ServiceConfig 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 getThis() { return this; }}
案例:
小结一下,对于Dubbo框架中的建造者模式,仅从URLBuilder类和ServiceBuilder类来看,它们确实有不少相似之处,都没有采用标准的,Joshua Bloch 推荐的建造者模式。也就是没有采用静态内部类的方式。创建的对象,也不是只读的,因为只读对象,具有很高的线程安全性,我估计是因为这些类,都是框架内部自用的,所以只考虑了用户正常使用的情况,而不会出现线程安全问题。另外,对泛型技术使用的比较熟练,看来进阿里大厂,不精通泛型是不行的。
至此,关于建造者模式,在开源软件JDK,Spring,Dubbo中的落地,我们就分享到这里。
后面,《源码说》系列,将分享单例模式,在开源软件中的使用情况。
大家对我的分享方式,或者分享内容,有什么意见或者建议,欢迎评论区留言或者私信我。我会不断进行改进,大家共同进步。
极客架构师,专注架构师成长,我们下期见。
留言与评论(共有 0 条评论) “” |