【平台化引擎-1】根据maven坐标—获取到jar的Class文件(URLClassLoader)

背景

低代码平台,需要实现一些UDF的能力,即用户在仓库或者平台上可以实现一块逻辑。然后会被打成SDK包,放到公司的私服仓库。

另一个服务在运行的时候,根据maven坐标与class名,动态的调用class中某些方法(该服务并不会显示的依赖这个maven坐标,而是通过动态加载的能力)

实现流程.png

实现

  1. 引入依赖(非必须,主要用于场景case的实现)
<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>
  1. 获取maven坐标的版本号:
  • 目的:提高操作体验,用户可以选择「最新Release版」「最新版」「指定版」的版本号。但如果用户未指定,需要去私服仓库的maven-metadata.xml配置中解析出版本号。

代码实现:

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;

/**
 * 根据坐标,获取到版本号
 */
@Slf4j
public class MavenVersionHelper {
    private static final String NEXUS_PREFIX = "http://私服仓库/nexus/content/groups/public/";
    private static final String NEXUS_SUBFIX = "/maven-metadata.xml";


    /**
     * 根据坐标,获取版本号。
     */
    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文件,以获取“真实”版本号信息 。“真实”表示的是,如果是Snapshot版本,那么jar应该存在时间戳后缀。
得到版本号后,下一步就可以拼接出来私服仓库的jar地址。

  1. 获取到私服的jar地址,创建出类加载器。
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关系
    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 {
            //根据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解析器代码

拉取最新的稳定版本


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 {

        //根据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");
        //反射调用某个方法
        Method method = clazz.getMethod("getDescriptor");
        //获取到pb的结构
        Descriptors.Descriptor descriptor = (Descriptors.Descriptor) method.invoke(null);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,812评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,626评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,144评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,052评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,925评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,035评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,461评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,150评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,413评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,501评论 2 307
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,277评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,159评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,528评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,868评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,143评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,407评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,615评论 2 335

推荐阅读更多精彩内容