通知的设计
1.通知的模块设计
将不同的通知方式设计为不同的插件,引入不同的插件包,并将支持的插件配置到数据库里,从数据库中load出来配置然后进行安装流程
2.插件的管理
AlertPluginManager
package org.apache.dolphinscheduler.alert.plugin;
import org.apache.dolphinscheduler.alert.api.AlertChannel;
import org.apache.dolphinscheduler.alert.api.AlertChannelFactory;
import org.apache.dolphinscheduler.alert.api.AlertConstants;
import org.apache.dolphinscheduler.common.enums.PluginType;
import org.apache.dolphinscheduler.common.enums.WarningType;
import org.apache.dolphinscheduler.dao.PluginDao;
import org.apache.dolphinscheduler.dao.entity.PluginDefine;
import org.apache.dolphinscheduler.spi.params.PluginParamsTransfer;
import org.apache.dolphinscheduler.spi.params.base.ParamsOptions;
import org.apache.dolphinscheduler.spi.params.base.PluginParams;
import org.apache.dolphinscheduler.spi.params.base.Validate;
import org.apache.dolphinscheduler.spi.params.radio.RadioParam;
import org.apache.dolphinscheduler.spi.plugin.PrioritySPIFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public final class AlertPluginManager {
private final PluginDao pluginDao;
public AlertPluginManager(PluginDao pluginDao) {
this.pluginDao = pluginDao;
}
private final Map<Integer, AlertChannel> alertPluginMap = new HashMap<>();
public void start() {
log.info("AlertPluginManager start ...");
checkAlertPluginExist(); // 检查插件是否存在
installAlertPlugin(); // 多抽象了一层安装插件的逻辑
log.info("AlertPluginManager started ...");
}
public Optional<AlertChannel> getAlertChannel(int id) {
return Optional.ofNullable(alertPluginMap.get(id));
}
public int size() {
return alertPluginMap.size();
}
private void checkAlertPluginExist() {
if (!pluginDao.checkPluginDefineTableExist()) {
log.error("Plugin Define Table t_ds_plugin_define Not Exist . Please Create it First !");
System.exit(1);
}
}
private void installAlertPlugin() {
final PluginParams warningTypeParams = getWarningTypeParams();
PrioritySPIFactory<AlertChannelFactory> prioritySPIFactory =
new PrioritySPIFactory<>(AlertChannelFactory.class);
for (Map.Entry<String, AlertChannelFactory> entry : prioritySPIFactory.getSPIMap().entrySet()) {
String name = entry.getKey();
AlertChannelFactory factory = entry.getValue(); // 找到对应的通信管道工厂
log.info("Registering alert plugin: {} - {}", name, factory.getClass());
final AlertChannel alertChannel = factory.create(); // 生成对应的通信管道
log.info("Registered alert plugin: {} - {}", name, factory.getClass());
final List<PluginParams> params = new ArrayList<>(factory.params());
params.add(0, warningTypeParams);
final String paramsJson = PluginParamsTransfer.transferParamsToJson(params);
final PluginDefine pluginDefine = new PluginDefine(name, PluginType.ALERT.getDesc(), paramsJson);
final int id = pluginDao.addOrUpdatePluginDefine(pluginDefine);
// 安装插件其实就是从库里load出来已支持的配置,然后加载成插件对象,放到map里
alertPluginMap.put(id, alertChannel);
}
}
private PluginParams getWarningTypeParams() {
return RadioParam.newBuilder(AlertConstants.NAME_WARNING_TYPE, AlertConstants.WARNING_TYPE)
.addParamsOptions(
new ParamsOptions(WarningType.SUCCESS.getDescp(), WarningType.SUCCESS.getDescp(), false))
.addParamsOptions(
new ParamsOptions(WarningType.FAILURE.getDescp(), WarningType.FAILURE.getDescp(), false))
.addParamsOptions(new ParamsOptions(WarningType.ALL.getDescp(), WarningType.ALL.getDescp(), false))
.setValue(WarningType.ALL.getDescp())
.addValidate(Validate.newBuilder().setRequired(true).build())
.build();
}
}
3.通知的抽象
3.1 定义通知管道接口
AlertChannel.java
public interface AlertChannel {
/**
* process and send alert
*
* @param info alert info
* @return process alarm result
*/
AlertResult process(AlertInfo info);
default @NonNull AlertResult closeAlert(AlertInfo info) {
return new AlertResult("true", "no need to close alert");
}
}
3.2 定义微信通知的实现管道
WeChatAlertChannel.java
public final class WeChatAlertChannel implements AlertChannel {
@Override
public AlertResult process(AlertInfo info) {
AlertData alertData = info.getAlertData();
Map<String, String> paramsMap = info.getAlertParams();
if (null == paramsMap) {
return new AlertResult("false", "we chat params is null");
}
return new WeChatSender(paramsMap).sendEnterpriseWeChat(alertData.getTitle(), alertData.getContent());
}
}
3.3 定义通知管道工厂,用来生成通知管道,不同的通知方式对应不同的工厂
AlertChannelFactory.java
public interface AlertChannelFactory extends PrioritySPI {
/**
* Returns the name of the alert channel
*
* @return the name of the alert channel
*/
String name();
/**
* Create an alert channel
*
* @return alert channel
*/
AlertChannel create();
/**
* Returns the configurable parameters that this plugin needs to display on the web ui
*/
List<PluginParams> params();
default SPIIdentify getIdentify() {
return SPIIdentify.builder().name(name()).build();
}
}
微信的通知管道工厂WeChatAlertChannelFactory.java
package org.apache.dolphinscheduler.plugin.alert.wechat;
import org.apache.dolphinscheduler.alert.api.AlertChannel;
import org.apache.dolphinscheduler.alert.api.AlertChannelFactory;
import org.apache.dolphinscheduler.alert.api.AlertConstants;
import org.apache.dolphinscheduler.alert.api.AlertInputTips;
import org.apache.dolphinscheduler.alert.api.ShowType;
import org.apache.dolphinscheduler.spi.params.base.ParamsOptions;
import org.apache.dolphinscheduler.spi.params.base.PluginParams;
import org.apache.dolphinscheduler.spi.params.base.Validate;
import org.apache.dolphinscheduler.spi.params.input.InputParam;
import org.apache.dolphinscheduler.spi.params.radio.RadioParam;
import java.util.Arrays;
import java.util.List;
import com.google.auto.service.AutoService;
@AutoService(AlertChannelFactory.class)
public final class WeChatAlertChannelFactory implements AlertChannelFactory {
@Override
public String name() {
return "WeChat";
}
@Override
public List<PluginParams> params() {
InputParam corpIdParam = InputParam
.newBuilder(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_CORP_ID,
WeChatAlertParamsConstants.ENTERPRISE_WE_CHAT_CORP_ID)
.setPlaceholder(AlertInputTips.CORP_ID.getMsg())
.addValidate(Validate.newBuilder()
.setRequired(true)
.build())
.build();
InputParam secretParam = InputParam
.newBuilder(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_SECRET,
WeChatAlertParamsConstants.ENTERPRISE_WE_CHAT_SECRET)
.setPlaceholder(AlertInputTips.SECRET.getMsg())
.addValidate(Validate.newBuilder()
.setRequired(true)
.build())
.build();
InputParam usersParam = InputParam
.newBuilder(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_USERS,
WeChatAlertParamsConstants.ENTERPRISE_WE_CHAT_USERS)
.setPlaceholder(AlertInputTips.WECHAT_MENTION_USERS.getMsg())
.addValidate(Validate.newBuilder()
.setRequired(false)
.build())
.build();
InputParam agentIdParam = InputParam
.newBuilder(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_AGENT_ID,
WeChatAlertParamsConstants.ENTERPRISE_WE_CHAT_AGENT_ID)
.setPlaceholder(AlertInputTips.WECHAT_AGENT_ID.getMsg())
.addValidate(Validate.newBuilder()
.setRequired(true)
.build())
.build();
RadioParam sendType = RadioParam
.newBuilder(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_SEND_TYPE,
WeChatAlertParamsConstants.ENTERPRISE_WE_CHAT_SEND_TYPE)
.addParamsOptions(new ParamsOptions(WeChatType.APP.getDescp(), WeChatType.APP.getDescp(), false))
.addParamsOptions(
new ParamsOptions(WeChatType.APPCHAT.getDescp(), WeChatType.APPCHAT.getDescp(), false))
.setValue(WeChatType.APP.getDescp())
.addValidate(Validate.newBuilder().setRequired(true).build())
.build();
RadioParam showType = RadioParam.newBuilder(AlertConstants.NAME_SHOW_TYPE, AlertConstants.SHOW_TYPE)
.addParamsOptions(new ParamsOptions(ShowType.MARKDOWN.getDescp(), ShowType.MARKDOWN.getDescp(), false))
.addParamsOptions(new ParamsOptions(ShowType.TEXT.getDescp(), ShowType.TEXT.getDescp(), false))
.setValue(ShowType.MARKDOWN.getDescp())
.addValidate(Validate.newBuilder().setRequired(true).build())
.build();
return Arrays.asList(corpIdParam, secretParam, usersParam, agentIdParam, sendType, showType);
}
@Override
public AlertChannel create() {
return new WeChatAlertChannel();
}
}
3.4 定义微信通知的发送器WeChatSender
WeChatSender.java
@Slf4j
public final class WeChatSender {
private static final String MUST_NOT_NULL = " must not null";
private static final String ALERT_STATUS = "false";
private static final String AGENT_ID_REG_EXP = "{agentId}";
private static final String MSG_REG_EXP = "{msg}";
private static final String USER_REG_EXP = "{toUser}";
private static final String CORP_ID_REGEX = "{corpId}";
private static final String SECRET_REGEX = "{secret}";
private static final String TOKEN_REGEX = "{token}";
private final String weChatAgentIdChatId;
private final String weChatUsers;
private final String weChatTokenUrlReplace;
private final String weChatToken;
private final String sendType;
private final String showType;
WeChatSender(Map<String, String> config) {
weChatAgentIdChatId = config.get(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_AGENT_ID);
weChatUsers = config.get(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_USERS);
String weChatCorpId = config.get(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_CORP_ID);
String weChatSecret = config.get(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_SECRET);
String weChatTokenUrl = WeChatAlertConstants.WE_CHAT_TOKEN_URL;
sendType = config.get(WeChatAlertParamsConstants.NAME_ENTERPRISE_WE_CHAT_SEND_TYPE);
showType = config.get(AlertConstants.NAME_SHOW_TYPE);
requireNonNull(showType, AlertConstants.NAME_SHOW_TYPE + MUST_NOT_NULL);
weChatTokenUrlReplace = weChatTokenUrl
.replace(CORP_ID_REGEX, weChatCorpId)
.replace(SECRET_REGEX, weChatSecret);
weChatToken = getToken();
}
private static String post(String url, String data) throws IOException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(data, WeChatAlertConstants.CHARSET));
CloseableHttpResponse response = httpClient.execute(httpPost);
String resp;
try {
HttpEntity entity = response.getEntity();
resp = EntityUtils.toString(entity, WeChatAlertConstants.CHARSET);
EntityUtils.consume(entity);
} finally {
response.close();
}
log.info("Enterprise WeChat send [{}], param:{}, resp:{}",
url, data, resp);
return resp;
}
}
/**
* convert text to markdown style
*
* @param title the title
* @param content the content
* @return markdown text
*/
private static String markdownText(String title, String content) {
if (StringUtils.isNotEmpty(content)) {
List<LinkedHashMap> mapItemsList = JSONUtils.toList(content, LinkedHashMap.class);
if (null == mapItemsList || mapItemsList.isEmpty()) {
log.error("itemsList is null");
throw new RuntimeException("itemsList is null");
}
StringBuilder contents = new StringBuilder(100);
contents.append(String.format("`%s`%n", title));
for (LinkedHashMap mapItems : mapItemsList) {
Set<Map.Entry<String, Object>> entries = mapItems.entrySet();
for (Entry<String, Object> entry : entries) {
contents.append(WeChatAlertConstants.MARKDOWN_QUOTE);
contents.append(entry.getKey()).append(":").append(entry.getValue());
contents.append(WeChatAlertConstants.MARKDOWN_ENTER);
}
}
return contents.toString();
}
return null;
}
private static String get(String url) throws IOException {
String resp;
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(url);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
HttpEntity entity = response.getEntity();
resp = EntityUtils.toString(entity, WeChatAlertConstants.CHARSET);
EntityUtils.consume(entity);
}
HashMap<String, Object> map = JSONUtils.parseObject(resp, HashMap.class);
if (map != null && null != map.get("access_token")) {
return map.get("access_token").toString();
} else {
return null;
}
}
}
private static String mkString(Iterable<String> list) {
if (null == list || StringUtils.isEmpty("|")) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String item : list) {
if (first) {
first = false;
} else {
sb.append("|");
}
sb.append(item);
}
return sb.toString();
}
private static AlertResult checkWeChatSendMsgResult(String result) {
AlertResult alertResult = new AlertResult();
alertResult.setStatus(ALERT_STATUS);
if (null == result) {
alertResult.setMessage("we chat send fail");
log.info("send we chat msg error,resp is null");
return alertResult;
}
WeChatSendMsgResponse sendMsgResponse = JSONUtils.parseObject(result, WeChatSendMsgResponse.class);
if (null == sendMsgResponse) {
alertResult.setMessage("we chat send fail");
log.info("send we chat msg error,resp error");
return alertResult;
}
if (sendMsgResponse.errcode == 0) {
alertResult.setStatus("true");
alertResult.setMessage("we chat alert send success");
return alertResult;
}
alertResult.setStatus(ALERT_STATUS);
alertResult.setMessage(sendMsgResponse.getErrmsg());
return alertResult;
}
/**
* send Enterprise WeChat
*
* @return Enterprise WeChat resp, demo: {"errcode":0,"errmsg":"ok","invaliduser":""}
*/
public AlertResult sendEnterpriseWeChat(String title, String content) {
AlertResult alertResult;
String data = markdownByAlert(title, content);
if (null == weChatToken) {
alertResult = new AlertResult();
alertResult.setMessage("send we chat alert fail,get weChat token error");
alertResult.setStatus(ALERT_STATUS);
return alertResult;
}
String enterpriseWeChatPushUrlReplace = "";
Map<String, String> contentMap = new HashMap<>();
contentMap.put(WeChatAlertConstants.WE_CHAT_CONTENT_KEY, data);
String msgJson = "";
if (sendType.equals(WeChatType.APP.getDescp())) {
enterpriseWeChatPushUrlReplace = WeChatAlertConstants.WE_CHAT_PUSH_URL.replace(TOKEN_REGEX, weChatToken);
WechatAppMessage wechatAppMessage = new WechatAppMessage(weChatUsers, showType,
Integer.valueOf(weChatAgentIdChatId), contentMap, WE_CHAT_MESSAGE_SAFE_PUBLICITY,
WE_CHAT_ENABLE_ID_TRANS, WE_CHAT_DUPLICATE_CHECK_INTERVAL_ZERO);
msgJson = JSONUtils.toJsonString(wechatAppMessage);
} else if (sendType.equals(WeChatType.APPCHAT.getDescp())) {
enterpriseWeChatPushUrlReplace =
WeChatAlertConstants.WE_CHAT_APP_CHAT_PUSH_URL.replace(TOKEN_REGEX, weChatToken);
WechatAppChatMessage wechatAppChatMessage =
new WechatAppChatMessage(weChatAgentIdChatId, showType, contentMap, WE_CHAT_MESSAGE_SAFE_PUBLICITY);
msgJson = JSONUtils.toJsonString(wechatAppChatMessage);
}
try {
return checkWeChatSendMsgResult(post(enterpriseWeChatPushUrlReplace, msgJson));
} catch (Exception e) {
log.info("send we chat alert msg exception : {}", e.getMessage());
alertResult = new AlertResult();
alertResult.setMessage("send we chat alert fail");
alertResult.setStatus(ALERT_STATUS);
}
return alertResult;
}
/**
* Determine the mardown style based on the show type of the alert
*
* @return the markdown alert table/text
*/
private String markdownByAlert(String title, String content) {
return markdownText(title, content);
}
private String getToken() {
try {
return get(weChatTokenUrlReplace);
} catch (IOException e) {
log.info("we chat alert get token error{}", e.getMessage());
}
return null;
}
static final class WeChatSendMsgResponse {
private Integer errcode;
private String errmsg;
public WeChatSendMsgResponse() {
}
public Integer getErrcode() {
return this.errcode;
}
public void setErrcode(Integer errcode) {
this.errcode = errcode;
}
public String getErrmsg() {
return this.errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof WeChatSendMsgResponse)) {
return false;
}
final WeChatSendMsgResponse other = (WeChatSendMsgResponse) o;
final Object this$errcode = this.getErrcode();
final Object other$errcode = other.getErrcode();
if (this$errcode == null ? other$errcode != null : !this$errcode.equals(other$errcode)) {
return false;
}
final Object this$errmsg = this.getErrmsg();
final Object other$errmsg = other.getErrmsg();
if (this$errmsg == null ? other$errmsg != null : !this$errmsg.equals(other$errmsg)) {
return false;
}
return true;
}
public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $errcode = this.getErrcode();
result = result * PRIME + ($errcode == null ? 43 : $errcode.hashCode());
final Object $errmsg = this.getErrmsg();
result = result * PRIME + ($errmsg == null ? 43 : $errmsg.hashCode());
return result;
}
public String toString() {
return "WeChatSender.WeChatSendMsgResponse(errcode=" + this.getErrcode() + ", errmsg=" + this.getErrmsg()
+ ")";
}
}
}
4.maven结构
4.1 dolphinscheduler-alert-wechat
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-alert-plugins</artifactId>
<version>dev-SNAPSHOT</version>
</parent>
<artifactId>dolphinscheduler-alert-wechat</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-alert-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
4.2 dolphinscheduler-alert-plugins
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-alert</artifactId>
<version>dev-SNAPSHOT</version>
</parent>
<artifactId>dolphinscheduler-alert-plugins</artifactId>
<packaging>pom</packaging>
<modules>
<module>dolphinscheduler-alert-all</module>
<module>dolphinscheduler-alert-api</module>
<module>dolphinscheduler-alert-email</module>
<module>dolphinscheduler-alert-wechat</module>
<module>dolphinscheduler-alert-dingtalk</module>
<module>dolphinscheduler-alert-script</module>
<module>dolphinscheduler-alert-http</module>
<module>dolphinscheduler-alert-feishu</module>
<module>dolphinscheduler-alert-slack</module>
<module>dolphinscheduler-alert-pagerduty</module>
<module>dolphinscheduler-alert-webexteams</module>
<module>dolphinscheduler-alert-telegram</module>
</modules>
</project>
4.3 dolphinscheduler-alert-server 通知服务后台,与dolphinscheduler-alert-plugins同级
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-alert</artifactId>
<version>dev-SNAPSHOT</version>
</parent>
<artifactId>dolphinscheduler-alert-server</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<dependencies>
<!-- dolphinscheduler -->
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-remote</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-meter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-alert-all</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-dao</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-registry-all</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-fabric8-config</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<excludes>
<exclude>*.yaml</exclude>
<exclude>*.xml</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>dolphinscheduler-alert-server</id>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<finalName>alert-server</finalName>
<descriptors>
<descriptor>src/main/assembly/dolphinscheduler-alert-server.xml</descriptor>
</descriptors>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>docker</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
4.4 dolphinscheduler-alert 通知模块最上层级
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler</artifactId>
<version>dev-SNAPSHOT</version>
</parent>
<artifactId>dolphinscheduler-alert</artifactId>
<packaging>pom</packaging>
<modules>
<module>dolphinscheduler-alert-plugins</module>
<module>dolphinscheduler-alert-server</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dolphinscheduler</groupId>
<artifactId>dolphinscheduler-bom</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
原创:做时间的朋友