本文基于 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 {
void start();
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:
8: public static final String SPRING_CONFIG = "dubbo.spring.config"; 9:
12: public static final String DEFAULT_SPRING_CONFIG = "classpath*:META-INF/spring/*.xml"; 13:
18: static ClassPathXmlApplicationContext context; 19: 20: public static ClassPathXmlApplicationContext getContext() { 21: return context; 22: } 23: 24: @Override 25: public void start() { 26: 27: String configPath = ConfigUtils.getProperty(SPRING_CONFIG); 28: if (configPath == null || configPath.length() == 0) { 29: configPath = DEFAULT_SPRING_CONFIG; 30: } 31: 32: context = new ClassPathXmlApplicationContext(configPath.split("[,\\s]+")); 33: 34: context.start(); 35: } 36: 37: @Override 38: public void stop() { 39: try { 40: if (context != null) { 41: 42: context.stop(); 43: 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:
6: public static final String LOG4J_FILE = "dubbo.log4j.file"; 7: 8:
11: public static final String LOG4J_SUBDIRECTORY = "dubbo.log4j.subdirectory"; 12: 13:
16: public static final String LOG4J_LEVEL = "dubbo.log4j.level"; 17:
20: public static final String DEFAULT_LOG4J_LEVEL = "ERROR"; 21: 22: @Override 23: @SuppressWarnings("unchecked") 24: public void start() { 25: 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: 34: Properties properties = new Properties(); 35: properties.setProperty("log4j.rootLogger", level + ",application"); 36: 37: properties.setProperty("log4j.appender.application", "org.apache.log4j.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: 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: 54: Enumeration<Appender> as = l.getAllAppenders(); 55: while (as.hasMoreElements()) { 56: Appender a = as.nextElement(); 57: if (a instanceof 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)) { 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 {
public static final String LOGBACK_FILE = "dubbo.logback.file";
public static final String LOGBACK_MAX_HISTORY = "dubbo.logback.maxhistory";
public static final String LOGBACK_LEVEL = "dubbo.logback.level";
public static final String DEFAULT_LOGBACK_LEVEL = "ERROR";
@Override public void start() { 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; } int maxHistory = StringUtils.parseInteger(ConfigUtils.getProperty(LOGBACK_MAX_HISTORY)); doInitializer(file, level, 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();
RollingFileAppender<ILoggingEvent> fileAppender = new RollingFileAppender<ILoggingEvent>(); fileAppender.setContext(loggerContext); fileAppender.setName("application"); fileAppender.setFile(file); fileAppender.setAppend(true);
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);
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:
8: public static final String CONTAINER_KEY = "dubbo.container"; 9: 10:
13: public static final String SHUTDOWN_HOOK_KEY = "dubbo.shutdown.hook"; 14: 15:
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: 27: if (args == null || args.length == 0) { 28: String config = ConfigUtils.getProperty(CONTAINER_KEY, loader.getDefaultExtensionName()); 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: 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: 55: LOCK.lock(); 56: 57: STOP.signal(); 58: } finally { 59: 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: 78: e.printStackTrace(); 79: logger.error(e.getMessage(), e); 80: System.exit(1); 81: } 82: try { 83: 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: 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. 彩蛋
推荐阅读:
知识星球