Ensure Dubbo can shutdown correctly when running both under tomcat and programmably.
diff --git a/all/pom.xml b/all/pom.xml
index 09bfe85..edc0c4a 100644
--- a/all/pom.xml
+++ b/all/pom.xml
@@ -321,6 +321,13 @@
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
+ <artifactId>dubbo-bootstrap</artifactId>
+ <version>${project.version}</version>
+ <scope>compile</scope>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba</groupId>
<artifactId>hessian-lite</artifactId>
<version>3.2.2</version>
<scope>compile</scope>
@@ -418,6 +425,7 @@
<include>com.alibaba:dubbo-serialization-fst</include>
<include>com.alibaba:dubbo-serialization-kryo</include>
<include>com.alibaba:dubbo-serialization-jdk</include>
+ <include>com.alibaba:dubbo-bootstrap</include>
</includes>
</artifactSet>
<transformers>
diff --git a/dubbo-bootstrap/pom.xml b/dubbo-bootstrap/pom.xml
new file mode 100644
index 0000000..e4320f0
--- /dev/null
+++ b/dubbo-bootstrap/pom.xml
@@ -0,0 +1,47 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>dubbo-parent</artifactId>
+ <groupId>com.alibaba</groupId>
+ <version>2.6.3-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>dubbo-bootstrap</artifactId>
+
+
+ <dependencies>
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>dubbo-config-api</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>dubbo-common</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>dubbo-registry-api</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
diff --git a/dubbo-bootstrap/src/main/java/org/apache/dubbo/bootstrap/DubboBootstrap.java b/dubbo-bootstrap/src/main/java/org/apache/dubbo/bootstrap/DubboBootstrap.java
new file mode 100644
index 0000000..8694e48
--- /dev/null
+++ b/dubbo-bootstrap/src/main/java/org/apache/dubbo/bootstrap/DubboBootstrap.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.bootstrap;
+
+import com.alibaba.dubbo.common.extension.ExtensionLoader;
+import com.alibaba.dubbo.common.logger.Logger;
+import com.alibaba.dubbo.common.logger.LoggerFactory;
+import com.alibaba.dubbo.config.ServiceConfig;
+import com.alibaba.dubbo.registry.support.AbstractRegistryFactory;
+import com.alibaba.dubbo.rpc.Protocol;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A bootstrap class to easily start and stop Dubbo via programmatic API.
+ * The bootstrap class will be responsible to cleanup the resources during stop.
+ */
+public class DubboBootstrap {
+
+ private static final Logger logger = LoggerFactory.getLogger(DubboBootstrap.class);
+
+ /**
+ * The list of ServiceConfig
+ */
+ private List<ServiceConfig> serviceConfigList;
+
+ /**
+ * Has it already been destroyed or not?
+ */
+ private final AtomicBoolean destroyed;
+
+ /**
+ * The shutdown hook used when Dubbo is running under embedded environment
+ */
+ private Thread shutdownHook;
+
+ public DubboBootstrap() {
+ this.serviceConfigList = new ArrayList<ServiceConfig>();
+ this.destroyed = new AtomicBoolean(false);
+ this.shutdownHook = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (logger.isInfoEnabled()) {
+ logger.info("Run shutdown hook now.");
+ }
+ destroy();
+ }
+ }, "DubboShutdownHook");
+ }
+
+ /**
+ * Register service config to bootstrap, which will be called during {@link DubboBootstrap#stop()}
+ * @param serviceConfig the service
+ * @return the bootstrap instance
+ */
+ public DubboBootstrap regsiterServiceConfig(ServiceConfig serviceConfig) {
+ serviceConfigList.add(serviceConfig);
+ return this;
+ }
+
+ public void start() {
+ registerShutdownHook();
+ for (ServiceConfig serviceConfig: serviceConfigList) {
+ serviceConfig.export();
+ }
+ }
+
+ public void stop() {
+ for (ServiceConfig serviceConfig: serviceConfigList) {
+ serviceConfig.unexport();
+ }
+ destroy();
+ removeShutdownHook();
+ }
+
+ /**
+ * Register the shutdown hook
+ */
+ public void registerShutdownHook() {
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ }
+
+ /**
+ * Remove this shutdown hook
+ */
+ public void removeShutdownHook() {
+ try {
+ Runtime.getRuntime().removeShutdownHook(shutdownHook);
+ }
+ catch (IllegalStateException ex) {
+ // ignore - VM is already shutting down
+ }
+ }
+
+ /**
+ * Destroy all the resources, including registries and protocols.
+ */
+ private void destroy() {
+ if (!destroyed.compareAndSet(false, true)) {
+ return;
+ }
+ // destroy all the registries
+ AbstractRegistryFactory.destroyAll();
+ // destroy all the protocols
+ ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
+ for (String protocolName : loader.getLoadedExtensions()) {
+ try {
+ Protocol protocol = loader.getLoadedExtension(protocolName);
+ if (protocol != null) {
+ protocol.destroy();
+ }
+ } catch (Throwable t) {
+ logger.warn(t.getMessage(), t);
+ }
+ }
+ }
+}
diff --git a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/AbstractConfig.java b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/AbstractConfig.java
index 73fc07d..8d8d182 100644
--- a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/AbstractConfig.java
+++ b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/AbstractConfig.java
@@ -73,18 +73,6 @@
legacyProperties.put("dubbo.service.url", "dubbo.service.address");
}
- static {
- Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
- @Override
- public void run() {
- if (logger.isInfoEnabled()) {
- logger.info("Run shutdown hook now.");
- }
- ProtocolConfig.destroyAll();
- }
- }, "DubboShutdownHook"));
- }
-
protected String id;
private static String convertLegacyValue(String key, String value) {
diff --git a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/ProtocolConfig.java b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/ProtocolConfig.java
index 0e7a524..b15129b 100644
--- a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/ProtocolConfig.java
+++ b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/ProtocolConfig.java
@@ -21,7 +21,6 @@
import com.alibaba.dubbo.common.status.StatusChecker;
import com.alibaba.dubbo.common.threadpool.ThreadPool;
import com.alibaba.dubbo.config.support.Parameter;
-import com.alibaba.dubbo.registry.support.AbstractRegistryFactory;
import com.alibaba.dubbo.remoting.Codec;
import com.alibaba.dubbo.remoting.Dispatcher;
import com.alibaba.dubbo.remoting.Transporter;
@@ -30,7 +29,6 @@
import com.alibaba.dubbo.rpc.Protocol;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
* ProtocolConfig
@@ -135,8 +133,6 @@
// if it's default
private Boolean isDefault;
- private static final AtomicBoolean destroyed = new AtomicBoolean(false);
-
public ProtocolConfig() {
}
@@ -149,27 +145,6 @@
setPort(port);
}
- // TODO: 2017/8/30 to move this method somewhere else
- public static void destroyAll() {
- if (!destroyed.compareAndSet(false, true)) {
- return;
- }
-
- AbstractRegistryFactory.destroyAll();
-
- ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
- for (String protocolName : loader.getLoadedExtensions()) {
- try {
- Protocol protocol = loader.getLoadedExtension(protocolName);
- if (protocol != null) {
- protocol.destroy();
- }
- } catch (Throwable t) {
- logger.warn(t.getMessage(), t);
- }
- }
- }
-
@Parameter(excluded = true)
public String getName() {
return name;
diff --git a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/RegistryConfig.java b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/RegistryConfig.java
index ed9db80..e5684f3 100644
--- a/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/RegistryConfig.java
+++ b/dubbo-config/dubbo-config-api/src/main/java/com/alibaba/dubbo/config/RegistryConfig.java
@@ -18,7 +18,6 @@
import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.config.support.Parameter;
-import com.alibaba.dubbo.registry.support.AbstractRegistryFactory;
import java.util.Map;
diff --git a/dubbo-config/dubbo-config-spring/pom.xml b/dubbo-config/dubbo-config-spring/pom.xml
index c7a6660..1ba2068 100644
--- a/dubbo-config/dubbo-config-spring/pom.xml
+++ b/dubbo-config/dubbo-config-spring/pom.xml
@@ -36,6 +36,11 @@
<version>${project.parent.version}</version>
</dependency>
<dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>dubbo-bootstrap</artifactId>
+ <version>${project.parent.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
@@ -44,6 +49,11 @@
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo-registry-default</artifactId>
<version>${project.parent.version}</version>
diff --git a/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListener.java b/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListener.java
new file mode 100644
index 0000000..43ee49d
--- /dev/null
+++ b/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboApplicationListener.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.dubbo.config.spring.initializer;
+
+import org.apache.dubbo.bootstrap.DubboBootstrap;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.context.event.ContextRefreshedEvent;
+
+/**
+ * An application listener that listens the ContextClosedEvent.
+ * Upon the event, this listener will do the necessary clean up to avoid memory leak.
+ */
+public class DubboApplicationListener implements ApplicationListener<ApplicationEvent> {
+
+ private DubboBootstrap dubboBootstrap;
+
+ public DubboApplicationListener() {
+ dubboBootstrap = new DubboBootstrap();
+ }
+
+ @Override
+ public void onApplicationEvent(ApplicationEvent applicationEvent) {
+ if (applicationEvent instanceof ContextRefreshedEvent) {
+ dubboBootstrap.start();
+ } else if (applicationEvent instanceof ContextClosedEvent) {
+ dubboBootstrap.stop();
+ }
+ }
+}
diff --git a/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboWebApplicationInitializer.java b/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboWebApplicationInitializer.java
new file mode 100644
index 0000000..0956d88
--- /dev/null
+++ b/dubbo-config/dubbo-config-spring/src/main/java/com/alibaba/dubbo/config/spring/initializer/DubboWebApplicationInitializer.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.dubbo.config.spring.initializer;
+
+import org.springframework.web.context.AbstractContextLoaderInitializer;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.XmlWebApplicationContext;
+
+
+public class DubboWebApplicationInitializer extends AbstractContextLoaderInitializer {
+
+ @Override
+ protected WebApplicationContext createRootApplicationContext() {
+ // TODO need to verify under spring-boot
+ XmlWebApplicationContext webApplicationContext = new XmlWebApplicationContext();
+ webApplicationContext.addApplicationListener(new DubboApplicationListener());
+ return webApplicationContext;
+ }
+}
diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/com/alibaba/dubbo/registry/support/FailbackRegistry.java b/dubbo-registry/dubbo-registry-api/src/main/java/com/alibaba/dubbo/registry/support/FailbackRegistry.java
index ced0efa..010e35f 100644
--- a/dubbo-registry/dubbo-registry-api/src/main/java/com/alibaba/dubbo/registry/support/FailbackRegistry.java
+++ b/dubbo-registry/dubbo-registry-api/src/main/java/com/alibaba/dubbo/registry/support/FailbackRegistry.java
@@ -29,6 +29,7 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
@@ -57,9 +58,14 @@
private final ConcurrentMap<URL, Map<NotifyListener, List<URL>>> failedNotified = new ConcurrentHashMap<URL, Map<NotifyListener, List<URL>>>();
+ /**
+ * The time in milliseconds the retryExecutor will wait
+ */
+ private final int retryPeriod;
+
public FailbackRegistry(URL url) {
super(url);
- int retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
+ this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
@@ -440,6 +446,32 @@
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
+ shutdownExecutorService(retryExecutor, retryPeriod);
+ }
+
+ /**
+ * Use the shutdown pattern from:
+ * https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
+ * @param pool the ExecutorService to be shutdown
+ * @param timeoutInMillis the timeout before terminication
+ */
+ protected void shutdownExecutorService(ExecutorService pool, int timeoutInMillis) {
+ // Disable new tasks from being submitted
+ pool.shutdown();
+ try {
+ // Wait a while for existing tasks to terminate
+ if (!pool.awaitTermination(timeoutInMillis, TimeUnit.MILLISECONDS)) {
+ pool.shutdownNow(); // Cancel currently executing tasks
+ // Wait a while for tasks to respond to being cancelled
+ if (!pool.awaitTermination(timeoutInMillis, TimeUnit.MILLISECONDS))
+ logger.warn("ExecutorService did not terminate: " + pool.toString());
+ }
+ } catch (InterruptedException ie) {
+ // (Re-)Cancel if current thread also interrupted
+ pool.shutdownNow();
+ // Preserve interrupt status
+ Thread.currentThread().interrupt();
+ }
}
// ==== Template method ====
diff --git a/dubbo-registry/dubbo-registry-default/src/main/java/com/alibaba/dubbo/registry/dubbo/DubboRegistry.java b/dubbo-registry/dubbo-registry-default/src/main/java/com/alibaba/dubbo/registry/dubbo/DubboRegistry.java
index 9d5ab70..8f6c0c8 100644
--- a/dubbo-registry/dubbo-registry-default/src/main/java/com/alibaba/dubbo/registry/dubbo/DubboRegistry.java
+++ b/dubbo-registry/dubbo-registry-default/src/main/java/com/alibaba/dubbo/registry/dubbo/DubboRegistry.java
@@ -47,7 +47,7 @@
private static final int RECONNECT_PERIOD_DEFAULT = 3 * 1000;
// Scheduled executor service
- private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1, new NamedThreadFactory("DubboRegistryReconnectTimer", true));
+ private final ScheduledExecutorService reconnectTimer = Executors.newScheduledThreadPool(1, new NamedThreadFactory("DubboRegistryReconnectTimer", true));
// Reconnection timer, regular check connection is available. If unavailable, unlimited reconnection.
private final ScheduledFuture<?> reconnectFuture;
@@ -59,13 +59,18 @@
private final RegistryService registryService;
+ /**
+ * The time in milliseconds the reconnectTimer will wait
+ */
+ private final int reconnectPeriod;
+
public DubboRegistry(Invoker<RegistryService> registryInvoker, RegistryService registryService) {
super(registryInvoker.getUrl());
this.registryInvoker = registryInvoker;
this.registryService = registryService;
// Start reconnection timer
- int reconnectPeriod = registryInvoker.getUrl().getParameter(Constants.REGISTRY_RECONNECT_PERIOD_KEY, RECONNECT_PERIOD_DEFAULT);
- reconnectFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
+ this.reconnectPeriod = registryInvoker.getUrl().getParameter(Constants.REGISTRY_RECONNECT_PERIOD_KEY, RECONNECT_PERIOD_DEFAULT);
+ reconnectFuture = reconnectTimer.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// Check and connect to the registry
@@ -127,6 +132,7 @@
logger.warn("Failed to cancel reconnect timer", t);
}
registryInvoker.destroy();
+ shutdownExecutorService(reconnectTimer, reconnectPeriod);
}
@Override
diff --git a/dubbo-registry/dubbo-registry-multicast/src/main/java/com/alibaba/dubbo/registry/multicast/MulticastRegistry.java b/dubbo-registry/dubbo-registry-multicast/src/main/java/com/alibaba/dubbo/registry/multicast/MulticastRegistry.java
index aa499fc..5f836ff 100644
--- a/dubbo-registry/dubbo-registry-multicast/src/main/java/com/alibaba/dubbo/registry/multicast/MulticastRegistry.java
+++ b/dubbo-registry/dubbo-registry-multicast/src/main/java/com/alibaba/dubbo/registry/multicast/MulticastRegistry.java
@@ -314,6 +314,7 @@
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
+ shutdownExecutorService(cleanExecutor, cleanPeriod);
}
protected void registered(URL url) {
diff --git a/dubbo-registry/dubbo-registry-redis/src/main/java/com/alibaba/dubbo/registry/redis/RedisRegistry.java b/dubbo-registry/dubbo-registry-redis/src/main/java/com/alibaba/dubbo/registry/redis/RedisRegistry.java
index ea4b22c..0f75cf6 100644
--- a/dubbo-registry/dubbo-registry-redis/src/main/java/com/alibaba/dubbo/registry/redis/RedisRegistry.java
+++ b/dubbo-registry/dubbo-registry-redis/src/main/java/com/alibaba/dubbo/registry/redis/RedisRegistry.java
@@ -263,6 +263,7 @@
logger.warn("Failed to destroy the redis registry client. registry: " + entry.getKey() + ", cause: " + t.getMessage(), t);
}
}
+ shutdownExecutorService(expireExecutor, expirePeriod);
}
@Override
diff --git a/pom.xml b/pom.xml
index 856eca6..9af7c4c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -133,6 +133,7 @@
<module>dubbo-demo</module>
<module>dubbo-plugin</module>
<module>dubbo-serialization</module>
+ <module>dubbo-bootstrap</module>
<module>dependencies-bom</module>
<module>bom</module>
<module>all</module>