背景
低代碼平臺,需要實現(xiàn)一些UDF的能力,即用戶在倉庫或者平臺上可以實現(xiàn)一塊邏輯。然后會被打成SDK包,放到公司的私服倉庫。
另一個服務(wù)在運行的時候,根據(jù)maven坐標與class名,動態(tài)的調(diào)用class中某些方法(該服務(wù)并不會顯示的依賴這個maven坐標,而是通過動態(tài)加載的能力)

實現(xiàn)流程.png
實現(xiàn)
- 引入依賴(非必須,主要用于場景case的實現(xiàn))
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.16.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
- 獲取maven坐標的版本號:
- 目的:提高操作體驗,用戶可以選擇「最新Release版」「最新版」「指定版」的版本號。但如果用戶未指定,需要去私服倉庫的
maven-metadata.xml配置中解析出版本號。
代碼實現(xiàn):
import com.tellme.maven.sax.MavenMetaSAXHandler;
import com.tellme.maven.sax.MavenSnapshotMetaSAXHandler;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
/**
* 根據(jù)坐標,獲取到版本號
*/
@Slf4j
public class MavenVersionHelper {
private static final String NEXUS_PREFIX = "http://私服倉庫/nexus/content/groups/public/";
private static final String NEXUS_SUBFIX = "/maven-metadata.xml";
/**
* 根據(jù)坐標,獲取版本號。
*/
public static MavenVersion getVersion(String groupId, String artifactId) {
try {
String metaUrl = buildMavenMetaDataFilePath(groupId, artifactId);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
MavenMetaSAXHandler handler = new MavenMetaSAXHandler();
parser.parse(metaUrl, handler);
return MavenVersion.builder().releaseVersion(handler.getReleaseVersion())
.latestVersion(handler.getLatestVersion()).build();
} catch (Exception e) {
log.error("getVersion error", e);
return null;
}
}
/**
* 對于快照版本,獲取到真實jarPath.
*/
public static String getRealVersionForSnapshot(String groupId, String artifactId, String latestVersion) throws Exception {
String metaUrl = buildMavenMetaDataFilePath(groupId, artifactId, latestVersion);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
MavenSnapshotMetaSAXHandler handler = new MavenSnapshotMetaSAXHandler();
parser.parse(metaUrl, handler);
//拿到真實的版本地址(時間戳)目的是拼接jar地址。
return latestVersion.replace("SNAPSHOT",
handler.getTimestamp() + "-" + handler.getBuildNumber());
}
private static String buildMavenMetaDataFilePath(String groupId, String artifactId) {
String replaceGroupId = String.join("/", groupId.split("\\."));
return String.format(NEXUS_PREFIX + "%s/%s/" + NEXUS_SUBFIX, replaceGroupId, artifactId);
}
private static String buildMavenMetaDataFilePath(String groupId, String artifactId, String version) {
groupId = String.join("/", groupId.split("\\."));
return String.format(NEXUS_PREFIX + "%s/%s/%s/" + NEXUS_SUBFIX, groupId, artifactId, version);
}
@Getter
@Builder
@ToString
public static class MavenVersion {
private String version;
private String releaseVersion;
private String latestVersion;
}
}
上面代碼,是獲取到maven-metadata.xml配置,需要解析XML文件,以獲取“真實”版本號信息 ?!罢鎸崱北硎镜氖牵绻荢napshot版本,那么jar應(yīng)該存在時間戳后綴。
得到版本號后,下一步就可以拼接出來私服倉庫的jar地址。
- 獲取到私服的jar地址,創(chuàng)建出類加載器。
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.tellme.maven.PbVersionTypeEnum;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.boot.loader.LaunchedURLClassLoader;
import java.net.URL;
import java.net.URLClassLoader;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static com.tellme.maven.helper.MavenVersionHelper.getRealVersionForSnapshot;
import static com.tellme.maven.helper.MavenVersionHelper.getVersion;
@Slf4j
public class MavenJarHelper1 {
private static final Integer EXPIRE_SECONDS = 60;
private static final Integer MAXIMUM_SIZE = 10000;
private static final String NEXUS_PREFIX = "http://私服地址/nexus/content/groups/public/";
//處理GRPC的PB中import關(guān)系
public static Map<String, List<MavenDependency>> loadDependency = new HashMap<>();
//引入jar的依賴項配置
static {
loadDependency.put("parent-artifactId", Lists.newArrayList(
new MavenDependency("com", "artifactId-1"),
new MavenDependency("com", "artifactId-2")
));
}
private static final LoadingCache<Pair<String, String>, String> JAR_RELEASE_CACHE = CacheBuilder.newBuilder()
.concurrencyLevel(5)
.expireAfterWrite(Duration.ofSeconds(EXPIRE_SECONDS))
.maximumSize(MAXIMUM_SIZE)
.build(new CacheLoader<Pair<String, String>, String>() {
@Override
public String load(Pair<String, String> param) throws Exception {
String groupId = param.getLeft();
String artifactId = param.getRight();
return getReleaseJarUrl(groupId, artifactId);
}
});
private static final LoadingCache<Pair<String, String>, String> JAR_LATEST_CACHE = CacheBuilder.newBuilder()
.concurrencyLevel(5)
.expireAfterWrite(Duration.ofSeconds(EXPIRE_SECONDS))
.maximumSize(MAXIMUM_SIZE)
.build(new CacheLoader<Pair<String, String>, String>() {
@Override
public String load(Pair<String, String> param) throws Exception {
String groupId = param.getLeft();
String artifactId = param.getRight();
return getLatestJarUrl(groupId, artifactId);
}
});
/**
* 單純maven坐標的類加載器
*/
private static final LoadingCache<String, URLClassLoader> CLASSLOADER_CACHE = CacheBuilder.newBuilder()
.concurrencyLevel(5)
.expireAfterWrite(Duration.ofSeconds(EXPIRE_SECONDS))
.maximumSize(MAXIMUM_SIZE)
.build(new CacheLoader<String, URLClassLoader>() {
@Override
public URLClassLoader load(String jarUrl) throws Exception {
try {
URL[] urls = new URL[]{new URL(jarUrl)};
return new LaunchedURLClassLoader(urls, MavenJarHelper1.class.getClassLoader());
} catch (Exception e) {
log.error("類庫加載失敗", e);
return null;
}
}
});
/**
* 加載依賴項的類加載器
*/
private static final LoadingCache<Pair<String, String>, URLClassLoader> CLASSLOADER_DEPENDENCY_CACHE =
CacheBuilder.newBuilder()
.concurrencyLevel(5)
.expireAfterWrite(Duration.ofSeconds(EXPIRE_SECONDS))
.maximumSize(MAXIMUM_SIZE)
.build(new CacheLoader<Pair<String, String>, URLClassLoader>() {
@Override
public URLClassLoader load(Pair<String, String> param) throws Exception {
try {
String groupId = param.getLeft();
String artifactId = param.getRight();
String jarUrl = getLatestJarUrl(groupId, artifactId);
if (StringUtils.isEmpty(jarUrl)) {
return null;
}
List<URL> urlList = Lists.newArrayList(new URL(jarUrl));
//讀取需要加載的依賴項
List<MavenDependency> dependencyList = loadDependency.get(artifactId);
if (CollectionUtils.isNotEmpty(dependencyList)) {
for (MavenDependency dependency : dependencyList) {
String dependencyUrl;
if (StringUtils.isEmpty(dependency.getVersion())) {
dependencyUrl = getLatestJarUrl(dependency.getGroupId(),
dependency.getArtifactId());
} else {
dependencyUrl = getJarUrlByCoordinate(dependency.getGroupId(), dependency.getArtifactId(),
dependency.getVersion());
}
if (StringUtils.isNotEmpty(dependencyUrl)) {
urlList.add(new URL(dependencyUrl));
}
}
}
return new LaunchedURLClassLoader(urlList.toArray(new URL[0]),
MavenJarHelper1.class.getClassLoader());
} catch (Exception e) {
log.error("類庫加載失敗", e);
return null;
}
}
});
private static final LoadingCache<Pair<MavenCoordinate, Integer>, URLClassLoader> CLASSLOADER_DEPENDENCY_COORDINATE =
CacheBuilder.newBuilder()
.concurrencyLevel(5)
.expireAfterWrite(Duration.ofSeconds(EXPIRE_SECONDS))
.maximumSize(MAXIMUM_SIZE)
.build(new CacheLoader<Pair<MavenCoordinate, Integer>, URLClassLoader>() {
@Override
public URLClassLoader load(Pair<MavenCoordinate, Integer> param) throws Exception {
try {
int versionType = param.getRight();
MavenCoordinate mavenCoordinate = param.getLeft();
log.info("mavenCoordinate: {}", mavenCoordinate);
String groupId = mavenCoordinate.getGroupId();
String artifactId = mavenCoordinate.getArtifactId();
String jarUrl = "";
//自定義版本
if (versionType == PbVersionTypeEnum.CUSTOMIZE_VERSION.getCode()) {
jarUrl = getJarUrlByCoordinate(groupId, artifactId, mavenCoordinate.getVersion());
}
//最新release版本
if (versionType == PbVersionTypeEnum.LAST_RELEASE_VERSION.getCode()) {
jarUrl = getReleaseJarUrlWithCache(groupId, artifactId);
}
//最新版本
if (versionType == PbVersionTypeEnum.LASTED_VERSION.getCode()
|| StringUtils.isEmpty(jarUrl) || versionType == 0) {
jarUrl = getLatestJarUrl(groupId, artifactId);
}
if (StringUtils.isEmpty(jarUrl)) {
return null;
}
log.info("jarUrl: {}", jarUrl);
List<URL> urlList = Lists.newArrayList(new URL(jarUrl));
//讀取依賴的jar
List<MavenDependency> dependencyList = loadDependency.get(artifactId);
if (CollectionUtils.isNotEmpty(dependencyList)) {
for (MavenDependency dependency : dependencyList) {
String dependencyUrl = getLatestJarUrl(dependency.getGroupId(),
dependency.getArtifactId());
if (StringUtils.isNotEmpty(dependencyUrl)) {
urlList.add(new URL(dependencyUrl));
}
}
}
return new LaunchedURLClassLoader(urlList.toArray(new URL[0]),
MavenJarHelper1.class.getClassLoader());
} catch (Exception e) {
log.error("類庫加載失敗", e);
return null;
}
}
});
public static String getReleaseJarUrlWithCache(String groupId, String artifactId) {
try {
return JAR_RELEASE_CACHE.get(Pair.of(groupId, artifactId));
} catch (Exception e) {
log.error("getReleaseJarUrlWithCache error", e);
return null;
}
}
public static String getLatestJarUrlWithCache(String groupId, String artifactId) {
try {
return JAR_LATEST_CACHE.get(Pair.of(groupId, artifactId));
} catch (Exception e) {
log.error("getReleaseJarUrlWithCache error", e);
return null;
}
}
public static URLClassLoader getClassLoaderCache(String jarUrl) {
try {
return CLASSLOADER_CACHE.get(jarUrl);
} catch (Exception e) {
log.error("getClassLoaderCache error", e);
return null;
}
}
public static URLClassLoader getClassLoaderDependencyCache(String groupId, String artifactId) {
try {
return CLASSLOADER_DEPENDENCY_CACHE.get(Pair.of(groupId, artifactId));
} catch (Exception e) {
log.error("getClassLoaderCache error", e);
return null;
}
}
public static URLClassLoader getClassLoaderByCoordinateCache(String groupId, String artifact, int versionType, String version) {
try {
MavenCoordinate mavenCoordinate = MavenCoordinate.builder()
.groupId(groupId)
.artifactId(artifact)
.version(version)
.build();
return CLASSLOADER_DEPENDENCY_COORDINATE.get(Pair.of(mavenCoordinate, versionType));
} catch (Exception e) {
log.error("pb getClassLoader error: ", e);
return null;
}
}
private static String getReleaseJarUrl(String groupId, String artifactId) {
try {
//根據(jù)maven坐標,獲取到maven的pom文件,獲取到最新的版本號
MavenVersionHelper.MavenVersion mavenVersion = getVersion(groupId, artifactId);
if (Objects.isNull(mavenVersion)) {
return null;
}
String releaseVersion = mavenVersion.getReleaseVersion();
if (StringUtils.isEmpty(releaseVersion)) {
return null;
}
return buildRemoteJarFilePath(groupId, artifactId, releaseVersion, releaseVersion);
} catch (Exception e) {
log.error("getReleaseJarUrl error", e);
return null;
}
}
private static String getLatestJarUrl(String groupId, String artifactId) {
try {
MavenVersionHelper.MavenVersion mavenVersion = getVersion(groupId, artifactId);
if (Objects.isNull(mavenVersion)) {
return null;
}
String latestVersion = mavenVersion.getLatestVersion();
if (StringUtils.isEmpty(latestVersion)) {
return null;
}
String realVersion = latestVersion;
if (latestVersion.contains("SNAPSHOT")) {
realVersion = getRealVersionForSnapshot(groupId, artifactId, latestVersion);
}
return buildRemoteJarFilePath(groupId, artifactId, latestVersion, realVersion);
} catch (Exception e) {
log.error("getLatestJarUrl error", e);
return null;
}
}
public static String getJarUrlByCoordinate(String groupId, String artifactId, String version) {
try {
if (StringUtils.isBlank(version)) {
return getLatestJarUrl(groupId, artifactId);
}
MavenVersionHelper.MavenVersion mavenVersion = MavenVersionHelper.MavenVersion.builder().version(version).build();
String customizeVersion = mavenVersion.getVersion();
String realVersion = customizeVersion;
if (customizeVersion.contains("SNAPSHOT")) {
realVersion = getRealVersionForSnapshot(groupId, artifactId, customizeVersion);
}
return buildRemoteJarFilePath(groupId, artifactId, customizeVersion, realVersion);
} catch (Exception e) {
log.error("getLatestJarUrl error", e);
return null;
}
}
/**
* 拼接URL地址
*/
private static String buildRemoteJarFilePath(String groupId, String artifactId, String version,
String realVersion) {
groupId = String.join("/", groupId.split("\\."));
return String.format(NEXUS_PREFIX + "%s/%s/%s/%s-%s.jar", groupId, artifactId, version, artifactId,
realVersion);
}
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public static class MavenDependency {
private String groupId;
private String version;
private String artifactId;
public MavenDependency(String groupId, String artifactId) {
this.groupId = groupId;
this.artifactId = artifactId;
}
}
@Data
@Builder
public static class MavenCoordinate {
private String groupId;
private String artifactId;
private String version;
private String scope;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MavenCoordinate that = (MavenCoordinate) o;
return Objects.equals(groupId, that.groupId) && Objects.equals(artifactId, that.artifactId);
}
@Override
public int hashCode() {
return Objects.hash(groupId, artifactId);
}
}
}
共用枚舉
public enum PbVersionTypeEnum {
LASTED_VERSION(1, "全部最新版"),
LAST_RELEASE_VERSION(2, "Release最新版"),
CUSTOMIZE_VERSION(3, "自定義版本");
}
xml解析器代碼
拉取最新的穩(wěn)定版本
import lombok.Getter;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.Objects;
/**
* XML解析器,獲取版本坐標。
* <latest>xxx</latest>
* <release>xxx</release>
*/
@Getter
public class MavenMetaSAXHandler extends DefaultHandler {
private String currentTag;
private String releaseVersion;
private String latestVersion;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
currentTag = qName;
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (Objects.nonNull(currentTag) && currentTag.equals("release")) {
releaseVersion = new String(ch, start, length);
}
if (Objects.nonNull(currentTag) && currentTag.equals("latest")) {
latestVersion = new String(ch, start, length);
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
currentTag = null;
}
}
拉取快照版本:
import lombok.Getter;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.Objects;
/**
* XML格式
* <snapshot>
* <timestamp>20240123.093241</timestamp>
* <buildNumber>1</buildNumber>
* </snapshot>
*/
@Getter
public class MavenSnapshotMetaSAXHandler extends DefaultHandler {
private String currentTag;
private String timestamp;
private String buildNumber;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
currentTag = qName;
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (Objects.nonNull(currentTag) && currentTag.equals("timestamp")) {
timestamp = new String(ch, start, length);
}
if (Objects.nonNull(currentTag) && currentTag.equals("buildNumber")) {
buildNumber = new String(ch, start, length);
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
currentTag = null;
}
}
使用方式
public class Test {
public static void main(String[] args) throws Exception {
//根據(jù)maven坐標,將jar讀取到本地容器中
URLClassLoader classLoader = MavenJarHelper.getClassLoaderByCoordinateCache("com.tellme",
"model", 3, "3.12.0");
if (Objects.isNull(classLoader)) {
System.out.println("classLoader 不存在");
return;
}
//加載某個類(PB文件)
Class<?> clazz = classLoader.loadClass("com.tellme.model.protobuf.AdIncModel$AdInstance");
//反射調(diào)用某個方法
Method method = clazz.getMethod("getDescriptor");
//獲取到pb的結(jié)構(gòu)
Descriptors.Descriptor descriptor = (Descriptors.Descriptor) method.invoke(null);
}
}