我是一段不羁的公告!
记得给艿艿这 3 个项目加油,添加一个 STAR 噢。
https://github.com/YunaiV/SpringBoot-Labs
https://github.com/YunaiV/onemall
https://github.com/YunaiV/ruoyi-vue-pro

精尽 Dubbo 源码分析 —— 服务容器

本文基于 Dubbo 2.6.1 版本,望知悉。

1. 概述

本文分享 Dubbo 实现 服务容器 功能。在 《Dubbo 用户指南 —— 服务容器》 定义如下:

服务容器是一个 standalone 的启动程序,因为后台服务不需要 Tomcat 或 JBoss 等 Web 容器的功能,如果硬要用 Web 容器去加载服务提供方,增加复杂性,也浪费资源。

服务容器只是一个简单的 Main 方法,并加载一个简单的 Spring 容器,用于暴露服务。

服务容器的加载内容可以扩展,内置了 spring, jetty, log4j 等加载,可通过容器扩展点进行扩展。配置配在 java 命令的 -D 参数或者 dubbo.properties 中。

  • 从概念上我们可以看到,和 SpringBoot 类似,是 Dubbo 服务的启动器。🙂 考虑到目前 Spring 更加通用,所以实际实践时,更多采用的是 SpringBoot ,而不是 Dubbo 的服务容器。
  • jetty 服务容器实现已经移除,新增 logback 服务容器实现。

本文涉及如下图所示:

一览

2. Container

com.alibaba.dubbo.container.Container ,服务容器接口

@SPI("spring")
public interface Container {

/**
* start.
*
* 启动
*/
void start();

/**
* stop.
*
* 停止
*/
void stop();

}
  • @SPI("spring") 注解,Dubbo SPI 拓展点,默认为 "spring"
  • 定义了容器的启动停止两个方法。

2.1 SpringContainer

com.alibaba.dubbo.container.spring.SpringContainer ,实现 Container 接口,Spring 容器实现类。属于 dubbo-container-spring 项目。

 1: public class SpringContainer implements Container {
2:
3: private static final Logger logger = LoggerFactory.getLogger(SpringContainer.class);
4:
5: /**
6: * Spring 配置属性 KEY
7: */
8: public static final String SPRING_CONFIG = "dubbo.spring.config";
9: /**
10: * 默认配置文件地址
11: */
12: public static final String DEFAULT_SPRING_CONFIG = "classpath*:META-INF/spring/*.xml";
13: /**
14: * Spring Context
15: *
16: * 静态属性,全局唯一
17: */
18: static ClassPathXmlApplicationContext context;
19:
20: public static ClassPathXmlApplicationContext getContext() {
21: return context;
22: }
23:
24: @Override
25: public void start() {
26: // 获得 Spring 配置文件的地址
27: String configPath = ConfigUtils.getProperty(SPRING_CONFIG);
28: if (configPath == null || configPath.length() == 0) {
29: configPath = DEFAULT_SPRING_CONFIG;
30: }
31: // 创建 Spring Context 对象
32: context = new ClassPathXmlApplicationContext(configPath.split("[,\\s]+"));
33: // 启动 Spring Context ,会触发 ContextStartedEvent 事件
34: context.start();
35: }
36:
37: @Override
38: public void stop() {
39: try {
40: if (context != null) {
41: // 停止 Spring Context ,会触发 ContextStoppedEvent 事件。
42: context.stop();
43: // 关闭 Spring Context ,会触发 ContextClosedEvent 事件。
44: context.close();
45: context = null;
46: }
47: } catch (Throwable e) {
48: logger.error(e.getMessage(), e);
49: }
50: }
51:
52: }
  • context 静态属性,Spring Context ,全局唯一。可通过 #getContext() 静态方法获取到。
  • SPRING_CONFIG 静态属性,Spring 配置属性 KEY
    • DEFAULT_SPRING_CONFIG 静态属性,默认 Spring 配置文件地址。
  • #start() 方法,启动 Spring 。代码如下:
    • 第 27 行:调用 ConfigUtils#getProperty(key) 方法,获得 Spring 配置文件的地址。优先级为:
      • 【高】JVM 启动参数:-Ddubbo.spring.config=自定义 XML 路径
      • 【低】Dubbo Properties 配置文件:dubbo.spring.config=自定义 XML 路径
    • 第 28 至 30 行:未配置,则使用默认路径 DEFAULT_SPRING_CONFIG
    • 第 32 行:创建 Spring Context 对象。
    • 第 34 行:调用 ClassPathXmlApplicationContext#start() 方法,启动 Spring Context 。通过 Spring 启动,加载我们的 Dubbo 配置,从而启动 Dubbo 服务。
  • #stop() 方法,关闭 Spring 。代码如下:
    • 第 42 行:调用 ClassPathXmlApplicationContext#stop() 方法,停止 Spring Context 。
    • 第 44 行:调用 ClassPathXmlApplicationContext#close() 方法,关闭 Spring Context 。

2.2 Log4jContainer

com.alibaba.dubbo.container.log4j.Log4jContainer ,实现 Container 接口,Log4j 容器实现类,自动配置 log4j 的配置,在多进程启动时,自动给日志文件按进程分目录。属于 dubbo-container-log4j 项目。

 1: public class Log4jContainer implements Container {
2:
3: /**
4: * 日志文件路径配置 KEY
5: */
6: public static final String LOG4J_FILE = "dubbo.log4j.file";
7:
8: /**
9: * 日志子目录径配置 KEY
10: */
11: public static final String LOG4J_SUBDIRECTORY = "dubbo.log4j.subdirectory";
12:
13: /**
14: * 日志级别配置 KEY
15: */
16: public static final String LOG4J_LEVEL = "dubbo.log4j.level";
17: /**
18: * 默认日志级别 - ERROR
19: */
20: public static final String DEFAULT_LOG4J_LEVEL = "ERROR";
21:
22: @Override
23: @SuppressWarnings("unchecked")
24: public void start() {
25: // 获得 log4j 配置的日志文件路径
26: String file = ConfigUtils.getProperty(LOG4J_FILE);
27: if (file != null && file.length() > 0) {
28: // 获得日志级别
29: String level = ConfigUtils.getProperty(LOG4J_LEVEL);
30: if (level == null || level.length() == 0) {
31: level = DEFAULT_LOG4J_LEVEL;
32: }
33: // 创建日志 Properties 对象,并设置到 PropertyConfigurator 中。
34: Properties properties = new Properties();
35: properties.setProperty("log4j.rootLogger", level + ",application"); // 日志级别
36: // log4j.appender.application 的配置
37: properties.setProperty("log4j.appender.application", "org.apache.log4j.DailyRollingFileAppender"); // DailyRollingFileAppender
38: properties.setProperty("log4j.appender.application.File", file); // 日志文件路径
39: properties.setProperty("log4j.appender.application.Append", "true");
40: properties.setProperty("log4j.appender.application.DatePattern", "'.'yyyy-MM-dd");
41: properties.setProperty("log4j.appender.application.layout", "org.apache.log4j.PatternLayout");
42: properties.setProperty("log4j.appender.application.layout.ConversionPattern", "%d [%t] %-5p %C{6} (%F:%L) - %m%n");
43: PropertyConfigurator.configure(properties);
44: }
45: // 获得日志子目录,用于多进程启动,避免冲突。
46: String subdirectory = ConfigUtils.getProperty(LOG4J_SUBDIRECTORY);
47: if (subdirectory != null && subdirectory.length() > 0) {
48: // 循环每个 Logger 对象
49: Enumeration<org.apache.log4j.Logger> ls = LogManager.getCurrentLoggers();
50: while (ls.hasMoreElements()) {
51: org.apache.log4j.Logger l = ls.nextElement();
52: if (l != null) {
53: // 循环每个 Logger 对象的 Appender 对象
54: Enumeration<Appender> as = l.getAllAppenders();
55: while (as.hasMoreElements()) {
56: Appender a = as.nextElement();
57: if (a instanceof FileAppender) { // 当且仅当 FileAppender 时
58: FileAppender fa = (FileAppender) a;
59: String f = fa.getFile();
60: if (f != null && f.length() > 0) {
61: int i = f.replace('\\', '/').lastIndexOf('/');
62: // 拼接日志子目录
63: String path;
64: if (i == -1) { // 无路径
65: path = subdirectory;
66: } else {
67: path = f.substring(0, i);
68: if (!path.endsWith(subdirectory)) { // 已经是 subdirectory 结尾,则不用拼接
69: path = path + "/" + subdirectory;
70: }
71: f = f.substring(i + 1);
72: }
73: // 设置新的文件名
74: fa.setFile(path + "/" + f);
75: // 生效配置
76: fa.activateOptions();
77: }
78: }
79: }
80: }
81: }
82: }
83: }
84:
85: @Override
86: public void stop() {
87: }
88:
89: }
  • LOG4J_FILE 静态属性,日志文件路径配置 KEY 。例如:dubbo.log4j.file=/foo/bar.log
  • LOG4J_SUBDIRECTORY 静态属性,日志子目录径配置 KEY 。例如:dubbo.log4j.subdirectory=20880
  • LOG4J_LEVEL 静态属性,日志级别配置 KEY。例如:dubbo.log4j.level=WARN
    • DEFAULT_LOG4J_LEVEL 静态属性, 默认日志级别,ERROR
  • #start() 方法,自动配置 log4j 的配置。代码如下:
    • 第 26 行:获得 log4j 配置的日志文件路径
    • 第 28 至 32 行:获得日志级别
    • 第 33 至 43 行:创建日志 Properties 对象,并设置到 org.apache.log4j.PropertyConfigurator 中。
    • 第 45 至 82 行:获得日志子目录,用于多进程启动,避免冲突。
  • #stop() 方法,空实现。因为,无需关闭。

2.3 LogbackContainer

com.alibaba.dubbo.container.logback.LogbackContainer ,实现 Container 接口,Logback 容器实现类,自动配置 logback 的配置,自动适配 logback 的配置。属于 dubbo-container-logback 项目。

public class LogbackContainer implements Container {

/**
* 日志文件路径配置 KEY
*/
public static final String LOGBACK_FILE = "dubbo.logback.file";

/**
* 日志保留天数配置 KEY
*/
public static final String LOGBACK_MAX_HISTORY = "dubbo.logback.maxhistory";

/**
* 日志级别配置 KEY
*/
public static final String LOGBACK_LEVEL = "dubbo.logback.level";
/**
* 默认日志级别 - ERROR
*/
public static final String DEFAULT_LOGBACK_LEVEL = "ERROR";

@Override
public void start() {
// 获得 logback 配置的日志文件路径
String file = ConfigUtils.getProperty(LOGBACK_FILE);
if (file != null && file.length() > 0) {
// 获得日志级别
String level = ConfigUtils.getProperty(LOGBACK_LEVEL);
if (level == null || level.length() == 0) {
level = DEFAULT_LOGBACK_LEVEL;
}
// 获得日志保留天数。若为零,则无限天数
// maxHistory=0 Infinite history
int maxHistory = StringUtils.parseInteger(ConfigUtils.getProperty(LOGBACK_MAX_HISTORY));
// 初始化 logback
doInitializer(file, level, maxHistory);
}
}

/**
* Initializer logback
*
* 初始化 logback
*
* @param file 日志文件路径
* @param level 日志级别
* @param maxHistory 日志保留天数
*/
private void doInitializer(String file, String level, int maxHistory) {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.detachAndStopAllAppenders();

// appender
RollingFileAppender<ILoggingEvent> fileAppender = new RollingFileAppender<ILoggingEvent>();
fileAppender.setContext(loggerContext);
fileAppender.setName("application");
fileAppender.setFile(file);
fileAppender.setAppend(true);

// policy
TimeBasedRollingPolicy<ILoggingEvent> policy = new TimeBasedRollingPolicy<ILoggingEvent>();
policy.setContext(loggerContext);
policy.setMaxHistory(maxHistory);
policy.setFileNamePattern(file + ".%d{yyyy-MM-dd}");
policy.setParent(fileAppender);
policy.start();
fileAppender.setRollingPolicy(policy);

// encoder
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(loggerContext);
encoder.setPattern("%date [%thread] %-5level %logger (%file:%line\\) - %msg%n");
encoder.start();
fileAppender.setEncoder(encoder);

fileAppender.start();

rootLogger.addAppender(fileAppender);
rootLogger.setLevel(Level.toLevel(level));
rootLogger.setAdditive(false);
}

@Override
public void stop() {
}

}
  • LOGBACK_FILE 静态属性,日志文件路径配置 KEY 。例如:dubbo.logback.file=/foo/bar.log
  • LOGBACK_MAX_HISTORY 静态属性,日志保留天数配置 KEY。例如:dubbo.logback.maxhistory=15
  • LOGBACK_LEVEL 静态属性,日志级别配置 KEY。例如:dubbo.logback.level=WARN
    • DEFAULT_LOGBACK_LEVEL 静态属性, 默认日志级别,ERROR
  • 代码比较简单,和 Log4jContainer 思路一致,胖友自己看注释。如果对 Logback 不了解的胖友,可以看看 《Logback背景》 文章。

3. Main

com.alibaba.dubbo.container.Main ,启动程序,负责初始化 Container 服务容器。代码如下:

 1: public class Main {
2:
3: private static final Logger logger = LoggerFactory.getLogger(Main.class);
4:
5: /**
6: * Container 配置 KEY
7: */
8: public static final String CONTAINER_KEY = "dubbo.container";
9:
10: /**
11: * ShutdownHook 是否开启配置 KEY
12: */
13: public static final String SHUTDOWN_HOOK_KEY = "dubbo.shutdown.hook";
14:
15: /**
16: * Container 拓展点对应的 ExtensionLoader 对象
17: */
18: private static final ExtensionLoader<Container> loader = ExtensionLoader.getExtensionLoader(Container.class);
19:
20: private static final ReentrantLock LOCK = new ReentrantLock();
21:
22: private static final Condition STOP = LOCK.newCondition();
23:
24: public static void main(String[] args) {
25: try {
26: // 若 main 函数参数传入为空,从配置中加载。
27: if (args == null || args.length == 0) {
28: String config = ConfigUtils.getProperty(CONTAINER_KEY, loader.getDefaultExtensionName()); // 默认 "spring"
29: args = Constants.COMMA_SPLIT_PATTERN.split(config);
30: }
31:
32: // 加载容器数组
33: final List<Container> containers = new ArrayList<Container>();
34: for (int i = 0; i < args.length; i++) {
35: containers.add(loader.getExtension(args[i]));
36: }
37: logger.info("Use container type(" + Arrays.toString(args) + ") to run dubbo serivce.");
38:
39: // ShutdownHook
40: if ("true".equals(System.getProperty(SHUTDOWN_HOOK_KEY))) {
41: Runtime.getRuntime().addShutdownHook(new Thread() {
42:
43: @Override
44: public void run() {
45: for (Container container : containers) {
46: // 关闭容器
47: try {
48: container.stop();
49: logger.info("Dubbo " + container.getClass().getSimpleName() + " stopped!");
50: } catch (Throwable t) {
51: logger.error(t.getMessage(), t);
52: }
53: try {
54: // 获得 ReentrantLock
55: LOCK.lock();
56: // 唤醒 Main 主线程的等待
57: STOP.signal();
58: } finally {
59: // 释放 ReentrantLock
60: LOCK.unlock();
61: }
62: }
63: }
64:
65: });
66: }
67:
68: // 启动容器
69: for (Container container : containers) {
70: container.start();
71: logger.info("Dubbo " + container.getClass().getSimpleName() + " started!");
72: }
73:
74: // 输出提示,启动成功
75: System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]").format(new Date()) + " Dubbo service server started!");
76: } catch (RuntimeException e) {
77: // 发生异常,JVM 退出
78: e.printStackTrace();
79: logger.error(e.getMessage(), e);
80: System.exit(1);
81: }
82: try {
83: // 获得 ReentrantLock
84: LOCK.lock();
85: // 释放锁,并且将自己沉睡,等待唤醒
86: STOP.await();
87: } catch (InterruptedException e) {
88: logger.warn("Dubbo service server stopped, interrupted by other thread!", e);
89: } finally {
90: // 释放 ReentrantLock
91: LOCK.unlock();
92: }
93: }
94:
95: }
  • CONTAINER_KEY 静态属性,Container 配置 KEY 。例如:dubbo.container=spring,jetty,log4j
  • SHUTDOWN_HOOK_KEY 静态属性,ShutdownHook 是否开启配置 KEY。例如:-Ddubbo.shutdown.hook=true
  • loader 静态属性,Container 拓展点对应的 ExtensionLoader 对象。
  • #main(args) 方法,初始化 Container 服务容器。代码如下:
    • 第 24 行:args 启动参数,可配置要加载的容器。例如,java com.alibaba.dubbo.container.Main spring jetty log4j
    • 第 26 至 30 行:若 args 为空,从配置中加载。例如:dubbo.container=spring,jetty,log4j 。若获取不到,使用 Container 的默认拓展 "spring"
    • 第 32 至 37 行:使用 Dubbo SPI 机制,加载配置的 Container 对象。
    • 第 68 至 72 行:循环调用 Container#start() 方法,启动容器。
    • 第 75 行:输出提示,启动成功
    • 第 76 至 81 行:若发生异常,打印错误日志,并 JVM 退出。
    • 第 84 行:调用 ReentrantLock#lock() 方法,获得 ReentrantLock 。
    • 第 86 行:调用 Condition#await() 方法,释放锁,并且将自己沉睡,等待唤醒
    • ========== ShutdownHook ==========
    • 第 40 至 41 行:当配置 JVM 启动参数带有 Ddubbo.shutdown.hook=true 时,添加关闭的 ShutdownHook 。
      • 当读到此处时,老艿艿就有个疑惑,如果不开启 ShutdownHook ,那岂不是 Main 一直等待,JVM 无法结束了?🙂 答案实际是不会,JVM 正常退出时,例如使用 kill pid 指定,只要 ShutdownHook 全部执行完成即可退出,无需 Main 函数执行完成。如果没有 ShutdownHook ,那就直接退出。
      • 那么 Main 的等待唤醒有什么作用?如果【第 86 行】不进行等待,Main 执行完成,就会触发 JVM 退出,导致 Dubbo 服务退出。所以相当于,起到了 JVM 进程常驻的作用。
    • 第 45 至 52 行:调用 Container#stop() 方法,关闭容器。
    • 第 53 至 61 行:调用 Condition#signal() 方法,唤醒 Main 的等待。
  • 早期版本 的 Main 实现,等待唤醒基于 Main.class 的 wait/notify 机制实现。考虑到安全性,Main.class#notify() 方法,可以被任意代码访问,导致非正常退出。所以改成了 ReentrantLock + Condition 来实现。值得借鉴。详细参见 《ISSUE#520 shutdown with count down latch》

4. 启动与暂停

dubbo-container-api 项目,resources/META-INF/assembly/bin/ 下,提供了脚本:

  • start.sh
  • stop.sh
  • restart.sh
  • dump.sh

还有一个 server.sh ,是根据参数,调用上述脚本。

🙂 神秘的微笑。详细解析,参见 《Dubbo应用启动与停止脚本,超详细解析》 文章。

666. 彩蛋

推荐阅读:

知识星球

总访客数 && 总访问量