1 | Dubbo:探讨标签路由的实现

项目需要,对 Dubbo 进行了一次功能调研,主要集中在服务治理中的标签路由。这部分的内容不难,但是能够对 Dubbo 的实现有一定的了解。

环境搭建

需要 ZooKeeper、Java Application Based on Dubbo。

ZooKeeper 的搭建

通过 CODING CD,在腾讯云的弹性伸缩组上(有兴趣可到 CODING CD 中详细了解),部署的一个实例,需要先安装 java 依赖,如下:

1
java-1.8.0-openjdk-devel.x86_64

其主要的配置是一个脚本,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wget http://mirrors.hust.edu.cn/apache/zookeeper/zookeeper-3.6.1/apache-zookeeper-3.6.1-bin.tar.gz
tar -xzvf apache-zookeeper-3.6.1-bin.tar.gz
mv apache-zookeeper-3.6.1-bin zk
mv zk/conf/zoo_sample.cfg zk/conf/zoo.cfg

echo '''[Unit]
Description=ZooKeeper for Dubbo
After=network.target
[Service]
Type=forking
ExecStart=/bin/sh /root/zk/bin/zkServer.sh start
ExecStop=/bin/sh /root/zk/bin/zkServer.sh stop
User=root
[Install]
WantedBy=multi-user.target ''' > /usr/lib/systemd/system/zk-for-dubbo.service
systemctl enable zk-for-dubbo

在安全组中确保端口 2181 开放。此时:

  • 公网 IP 为 49.233.238.170
  • 内网 IP 为 172.21.0.34

Java Application Based on Dubbo

A 模块在调用 B 模块前,设置了一个 TAG_KEY,表示选择带有 TAG_KEY 的 B 服务:

1
2
3
4
5
6
7
@Reference
private DoSomeThingService doSomeThingService;

public String sayHello() {
RpcContext.getContext().setAttachment(CommonConstants.TAG_KEY, "tag1");
return doSomeThingService.sayHello();
}

应用的部署还是采用 CODING CD 部署在腾讯云的弹性伸缩组上。主要的配置如下:

A 模块启动:

1
java -jar -Ddubbo.registry.address=zookeeper://172.21.0.34:2181 /root/a.jar 2>&1 &

B 模块启动:

1
java -jar -Ddubbo.registry.address=zookeeper://172.21.0.34:2181 /root/b.jar 2>&1 &

Dubbo admin 安装与配置

dubbo-admin 使用的 github 中 develop 的最新版,按照相应的提示进行构建即可。

启动 dubbo-admin 的前端,此时的目录为 dubbo-admin/dubbo-admin-ui

1
npm run dev

启动 dubbo-admin 的后端,此时的目录为 dubbo-admin/dubbo-admin-server

修改 dubbo-admin/dubbo-admin-server/src/main/resources/application.properties 文件,改成正确的 zookeeper 地址。

1
2
3
admin.registry.address=zookeeper://49.233.238.170:2181
admin.config-center=zookeeper://49.233.238.170:2181
admin.metadata-report.address=zookeeper://49.233.238.170:2181

编译并启动

1
2
3
mvn clean package -DskipTests=true
cd target
java -jar dubbo-admin-server-0.2.0-SNAPSHOT.jar

打开 dubbo-admin,找到标签路由,配置如下,应用名填写 moduleb:

1
2
3
4
5
6
7
8
9
enabled: true
force: true
runtime: true
tags:
- name: tag1
addresses:
- '172.21.0.40:20880'
- name: tag2
addresses: null

TIPS
如果有遇到在 Dubbo Admin 中修改不生效的情况,考虑一下 Dubbo Admin 与 Dubbo 版本间的差异。在此踩到一个坑如下:

Dubbo Admin 管理页面是用 docker 跑起来的,它里面的源代码比较旧,路由设置到 zk 中的路径与 dubbo 读取 zk 的路径不一样。ISSUE 可见:https://github.com/apache/dubbo-admin/issues/577

效果展示

请求流向:LB -> A -> B

A 中设置的 TAG_KEY 为:

1
2
3
4
public String sayHello() {
RpcContext.getContext().setAttachment(CommonConstants.TAG_KEY, "tag1");
return doSomeThingService.sayHello();
}

此时 B 模块服务有两个,即一个新、一个旧。

如果要只返回最新版本,可在 dubbo-admin 控制台的服务治理中,添加标签路由,新的实例在 tag1 下,旧的实例在 tag2 下,如下:

1
2
3
4
5
6
7
8
9
10
enabled: true
force: true
runtime: true
tags:
- name: tag1
addresses:
- '172.21.0.21:20880'
- name: tag2
addresses:
- '172.21.0.40:20880'

此时的返回如下:

如果只使用旧版本实例,可在将上面 tag1 和 tag2 下面的内容调换即可,此时返回如下:

如果要同时能够访问到新旧实例的内容,将上述配置中所有的 IP 全部填写到 tag1 下即可,此时的返回如下:
image

Dubbo 标签路由的实现原理

总共分三个大的方面,分别是配置的存储、配置同步、如何解析配置。

标签路由配置的存储

URL: /api/dev/rules/route/tag/moduleb
Request Method: PUT

在 dubbo-admin-server 中它的实现在文件 TagRoutesController.java 中,最终是将配置保存到 ZooKeeper 中,保存的路径为:

即:/dubbo/config/dubbo/moduleb.tag-router

在 ZooKeeper 中可以在相应的路径下看到相关的配置,如下:

配置变更同步

TagRouter.java 中实现了 ConfigurationListener 接口,当配置变更时,会更新 tagRouterRule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public synchronized void process(ConfigChangedEvent event) {
if (logger.isDebugEnabled()) {
logger.debug("Notification of tag rule, change type is: " + event.getChangeType() + ", raw rule is:\n " +
event.getContent());
}

try {
if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
this.tagRouterRule = null;
} else {
this.tagRouterRule = TagRuleParser.parse(event.getContent());
}
} catch (Exception e) {
logger.error("Failed to parse the raw tag router rule and it will not take effect, please check if the " +
"rule matches with the template, the raw rule is:\n ", e);
}
}

配置解析

源码位于 dubbo 中的 TagRouter.java

结语

在上面的示例中,我们需要手动在 dubbo-admin 界面中,新增标签路由的配置,但经过对该配置的新增过程的简要分析,发现只是在 ZooKeeper 中新增一条记录。

理解Servlet与Filter的关系与设计思路

什么是Servlet对一个HTTP请求的正常的处理流程是:发送HTTP请求服务端的HTTP服务器收到请求调用业务逻辑返回HTTP响应产生了下面3个问题:HTTP 服务器怎么知道要调用哪个业务逻辑,也就是 Java 类的哪个方法呢?HTTP服务器可以被设计成收到请求后,接续寻找该请求的处理逻辑,但是这
阅读更多

SpringMVC源码探索之RequestBody的工作原理

遇到一个很奇怪的问题,后面发现了问题所在,原因是自己太过匆忙、连快捷键都被复制粘贴省略了。虽然出现问题的原因有点傻逼,但是之所以出现这种问题的原因却更加引人入胜。问题现象描述Controller中没有逻辑,只有一个@RequestBody注释的form表单然而这个TestBean有点特殊,非一般的g
阅读更多

系统应用闹钟之TabLayout的使用

此篇文章记录一些关于安卓原生应用闹钟里面TabLayout的使用,没见过用这个组件的源代码,想从它里面学习学习。就当是见见世面,顺便写个笔记。

先从印象到布局

原生闹钟给我的印象非常好,有种说不出的美感。在改工作上的问题时,顺便看它的样式,大致就是一个TabLayout加上四个fragment

首先是头部的四个图标,是一个嵌在了Toolbar中的TabLayoutToolbar也是一个ViewGroup,在它的里面放一个其他的控件也是理所当然,只是以前没试过。第一次看到这个写法,感觉非常好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
app:elevation="0dp">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentInsetStart="0dp"
tools:ignore="RtlSymmetry">
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:tabGravity="fill"
app:tabIndicatorColor="@android:color/transparent"
app:tabMaxWidth="0dp"
app:tabMode="fixed"
app:tabPaddingEnd="0dp"
app:tabPaddingStart="0dp" />
</android.support.v7.widget.Toolbar>

然后在Toolbar下面就是一个ViewPager,存放在一个FrameLayout中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<android.support.v4.view.ViewPager
android:id="@+id/desk_clock_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:saveEnabled="false" />

<include layout="@layout/drop_shadow" />

</FrameLayout>

这样基本上大致的框架就出来了。接着看对它的初始化:
基本步骤和网上的教程是差不多的,只是其中的UiDataModel感觉非常有意思,感觉它几乎无所不能,用到的任何东西都可以往里面取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Create the tabs that make up the user interface.
mTabLayout = (TabLayout) findViewById(R.id.tabs);
final int tabCount = UiDataModel.getUiDataModel().getTabCount();
final boolean showTabLabel = getResources().getBoolean(R.bool.showTabLabel);
final boolean showTabHorizontally = getResources().getBoolean(R.bool.showTabHorizontally);
for (int i = 0; i < tabCount; i++) {
final UiDataModel.Tab tabModel = UiDataModel.getUiDataModel().getTab(i);
final @StringRes int labelResId = tabModel.getLabelResId();

final TabLayout.Tab tab = mTabLayout.newTab()
.setTag(tabModel)
.setIcon(tabModel.getIconResId())
.setContentDescription(labelResId);

if (showTabLabel) {
tab.setText(labelResId);
tab.setCustomView(R.layout.tab_item);

@SuppressWarnings("ConstantConditions")
final TextView text = (TextView) tab.getCustomView()
.findViewById(android.R.id.text1);
text.setTextColor(mTabLayout.getTabTextColors());

// Bind the icon to the TextView.
final Drawable icon = tab.getIcon();
if (showTabHorizontally) {
// Remove the icon so it doesn't affect the minimum TabLayout height.
tab.setIcon(null);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
} else {
text.setCompoundDrawablesRelativeWithIntrinsicBounds(null, icon, null, null);
}
}

打开UiDataModel,第一个引起我的关注的是一个枚举型,在其中保存了有关Tab的基本信息,如文本、图标资源id以及该Tab所对应的fragment等。第一次见到这种写法,有点震惊,同时觉得对于这种静态的应用,用枚举型去保存也符合常理,我觉得这种方法可以以后利用在我写的Android项目中,比如说我的应用中,Tab也是固定的,同时这样写可以让那些信息更加集中,修改起来更加方便,让代码的逻辑性更强。看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Identifies each of the primary tabs within the application. */
public enum Tab {
ALARMS(AlarmClockFragment.class, R.drawable.ic_tab_alarm, R.string.menu_alarm),
CLOCKS(ClockFragment.class, R.drawable.ic_tab_clock, R.string.menu_clock),
TIMERS(TimerFragment.class, R.drawable.ic_tab_timer, R.string.menu_timer),
STOPWATCH(StopwatchFragment.class, R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);

private final String mFragmentClassName;
private final @DrawableRes int mIconResId;
private final @StringRes int mLabelResId;

Tab(Class fragmentClass, @DrawableRes int iconResId, @StringRes int labelResId) {
mFragmentClassName = fragmentClass.getName();
mIconResId = iconResId;
mLabelResId = labelResId;
}

public String getFragmentClassName() { return mFragmentClassName; }
public @DrawableRes int getIconResId() { return mIconResId; }
public @StringRes int getLabelResId() { return mLabelResId; }
}

感觉自己简直被这源代码所陶醉。后面继续看关于TabLayout的初始化。这其中,对其的适配器的写法,我也是比较感兴趣的。因为自己在写应用的时候,遇到过关于PagerAdapter的问题,所以比较期待它是如何去实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Customize the view pager.
mFragmentTabPagerAdapter = new FragmentTabPagerAdapter(this);
mFragmentTabPager = (ViewPager) findViewById(R.id.desk_clock_pager);
// Keep all four tabs to minimize jank.
mFragmentTabPager.setOffscreenPageLimit(3);
// Set Accessibility Delegate to null so view pager doesn't intercept movements and
// prevent the fab from being selected.
mFragmentTabPager.setAccessibilityDelegate(null);
// Mirror changes made to the selected page of the view pager into UiDataModel.
mFragmentTabPager.addOnPageChangeListener(new PageChangeWatcher());
mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter);

// Mirror changes made to the selected tab into UiDataModel.
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
UiDataModel.getUiDataModel().setSelectedTab((UiDataModel.Tab) tab.getTag());
}

@Override
public void onTabUnselected(TabLayout.Tab tab) {
}

@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});

适配器的源代码不完全贴出来,感觉都对不起这源代码。它继承自ViewPager,自己实现了其中的方法,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* This adapter produces the DeskClockFragments that are the content of the DeskClock tabs. The
* adapter presents the tabs in LTR and RTL order depending on the text layout direction for the
* current locale. To prevent issues when switching between LTR and RTL, fragments are registered
* with the manager using position-independent tags, which is an important departure from
* FragmentPagerAdapter.
*/
final class FragmentTabPagerAdapter extends PagerAdapter {

private final DeskClock mDeskClock;

/** The manager into which fragments are added. */
private final FragmentManager mFragmentManager;

/** A fragment cache that can be accessed before {@link #instantiateItem} is called. */
private final Map<UiDataModel.Tab, DeskClockFragment> mFragmentCache;

/** The active fragment transaction if one exists. */
private FragmentTransaction mCurrentTransaction;

/** The current fragment displayed to the user. */
private Fragment mCurrentPrimaryItem;

FragmentTabPagerAdapter(DeskClock deskClock) {
mDeskClock = deskClock;
mFragmentCache = new ArrayMap<>(getCount());
mFragmentManager = deskClock.getFragmentManager();
}

@Override
public int getCount() {
return UiDataModel.getUiDataModel().getTabCount();
}

/**
* @param position the left-to-right index of the fragment to be returned
* @return the fragment displayed at the given {@code position}
*/
DeskClockFragment getDeskClockFragment(int position) {
// Fetch the tab the UiDataModel reports for the position.
final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);

// First check the local cache for the fragment.
DeskClockFragment fragment = mFragmentCache.get(tab);
if (fragment != null) {
return fragment;
}

// Next check the fragment manager; relevant when app is rebuilt after locale changes
// because this adapter will be new and mFragmentCache will be empty, but the fragment
// manager will retain the Fragments built on original application launch.
fragment = (DeskClockFragment) mFragmentManager.findFragmentByTag(tab.name());
if (fragment != null) {
fragment.setFabContainer(mDeskClock);
mFragmentCache.put(tab, fragment);
return fragment;
}

// Otherwise, build the fragment from scratch.
final String fragmentClassName = tab.getFragmentClassName();
fragment = (DeskClockFragment) Fragment.instantiate(mDeskClock, fragmentClassName);
fragment.setFabContainer(mDeskClock);
mFragmentCache.put(tab, fragment);
return fragment;
}

@Override
public void startUpdate(ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this + " has no id");
}
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}

// Use the fragment located in the fragment manager if one exists.
final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);
Fragment fragment = mFragmentManager.findFragmentByTag(tab.name());
if (fragment != null) {
mCurrentTransaction.attach(fragment);
} else {
fragment = getDeskClockFragment(position);
mCurrentTransaction.add(container.getId(), fragment, tab.name());
}

if (fragment != mCurrentPrimaryItem) {
FragmentCompat.setMenuVisibility(fragment, false);
FragmentCompat.setUserVisibleHint(fragment, false);
}

return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
final DeskClockFragment fragment = (DeskClockFragment) object;
fragment.setFabContainer(null);
mCurrentTransaction.detach(fragment);
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
final Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false);
FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false);
}
if (fragment != null) {
FragmentCompat.setMenuVisibility(fragment, true);
FragmentCompat.setUserVisibleHint(fragment, true);
}
mCurrentPrimaryItem = fragment;
}
}

@Override
public void finishUpdate(ViewGroup container) {
if (mCurrentTransaction != null) {
mCurrentTransaction.commitAllowingStateLoss();
mCurrentTransaction = null;
mFragmentManager.executePendingTransactions();
}
}

@Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment) object).getView() == view;
}
}