當前位置: 妍妍網 > 碼農

SpringBoot 動態載入 jar 包,動態配置方案

2024-04-01碼農

推薦關註

掃碼關註 後端架構師 」,選擇 星標 公眾號

重磅幹貨,第一時間送達!

責編:架構君 | 來源:小白

連結:https://blog.csdn.net/qq_45584746

上一篇好文:

正文

大家好,我是後端架構師。

一、概述

1、背景

目前數據治理服務中有眾多治理任務,當其中任一治理任務有改動需要升級或新增一個治理任務時,都需要將數據治理服務重新開機,會影響其他治理任務的正常執行。

2、目標

  • 能夠動態啟動、停止任一治理任務

  • 能夠動態升級、添加治理任務

  • 啟動、停止治理任務或升級、添加治理任務不能影響其他任務

  • 3、方案

  • 為了支持業務程式碼盡量的解耦,把部份業務功能透過動態載入的方式載入到主程式中,以滿足可插拔式的載入、組合式的部署。

  • 配合xxl-job任務排程框架,將數據治理任務做成xxl-job任務的方式註冊到xxl-job中,方便統一管理。

  • 二、動態載入

    1、自訂類載入器

    URL classLoader 是一種特殊的類載入器,可以從指定的 URL 中載入類和資源。它的主要作用是動態載入外部的 JAR 包或者類檔,從而實作動態擴充套件應用程式的功。為了便於管理動態載入的jar包,自訂類載入器繼承URL classloader。

    package cn.jy.sjzl.util;
    import java.lang.reflect.Method;
    import java.net.URL;
    import java.net.URL classLoader;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    /**
     * 自訂類載入器
     *
     * @author lijianyu
     * @date 2023/04/03 17:54
     **/

    public classMy classLoaderextendsURL classLoader{
    private Map<String, class<?>> loaded classes = new ConcurrentHashMap<>();
    public Map<String, class<?>> getLoaded classes() {
    return loaded classes;
    }
    publicMy classLoader(URL[] urls, classLoader parent){
    super(urls, parent);
    }
    @Override
    protected class<?> find class(String name) throws classNotFoundException {
    // 從已載入的類集合中獲取指定名稱的類
    class<?> clazz = loaded classes.get(name);
    if (clazz != null) {
    return clazz;
    }
    try {
    // 呼叫父類的find class方法載入指定名稱的類
    clazz = super.find class(name);
    // 將載入的類添加到已載入的類集合中
    loaded classes.put(name, clazz);
    return clazz;
    catch ( classNotFoundException e) {
    e.printStackTrace();
    returnnull;
    }
    }
    publicvoidunload(){
    try {
    for (Map.Entry<String, class<?>> entry : loaded classes.entrySet()) {
    // 從已載入的類集合中移除該類
    String className = entry.getKey();
    loaded classes.remove( className);
    try{
    // 呼叫該類的destory方法,回收資源
    class<?> clazz = entry.getValue();
    Method destory = clazz.getDeclaredMethod("destory");
    destory.invoke(clazz);
    catch (Exception e ) {
    // 表明該類沒有destory方法
    }
    }
    // 從其父類載入器的載入器階層中移除該類載入器
    close();
    catch (Exception e) {
    e.printStackTrace();
    }
    }
    }





  • 自訂類載入器中,為了方便類的解除安裝,定義一個map保存已載入的類資訊。key為這個類的 className,value為這個類的類資訊。

  • 同時定義了類載入器的解除安裝方法,解除安裝方法中,將已載入的類的集合中移除該類。由於此類可能使用系統資源或呼叫執行緒,為了避免資源未回收引起的記憶體溢位,透過反射呼叫這個類中的destroy方法,回收資源。

  • 最後呼叫close方法。

  • 2、動態載入

    由於此計畫使用spring框架,以及xxl-job任務的機制呼叫動態載入的程式碼,因此要完成以下內容

  • 將動態載入的jar包讀到記憶體中

  • 將有spring註解的類,透過註解掃描的方式,掃描並手動添加到spring容器中。

  • 將@XxlJob註解的方法,透過註解掃描的方式,手動添加到xxljob執行器中。

  • package com.jy.dynamicLoad;
    import com.jy.annotation.XxlJobCron;
    import com.jy. classLoader.My classLoader;
    import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
    import com.xxl.job.core.handler.annotation.XxlJob;
    import com.xxl.job.core.handler.impl.MethodJobHandler;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.beans.factory.support.AbstractBeanDefinition;
    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
    import org.springframework.beans.factory.support.DefaultListableBeanFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.core.MethodIntrospector;
    import org.springframework.core.annotation.AnnotatedElementUtils;
    import org.springframework.stereotype.Component;
    import java.io.File;
    import java.io.IOException;
    import java.lang.reflect.Method;
    import java.net.JarURLConnection;
    import java.net.URL;
    import java.net.URLConnection;
    import java.util.Enumeration;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.jar.JarEntry;
    import java.util.jar.JarFile;
    /**
     * @author lijianyu
     * @date 2023/04/29 13:18
     **/

    @Component
    public classDynamicLoad{
    privatestatic Logger logger = LoggerFactory.getLogger(DynamicLoad. class);
    @Autowired
    private ApplicationContext applicationContext;
    private Map<String, My classLoader> my classLoaderCenter = new ConcurrentHashMap<>();
    @Value("${dynamicLoad.path}")
    private String path;
    /**
    * 動態載入指定路徑下指定jar包
    @param path
    @param fileName
    @param isRegistXxlJob 是否需要註冊xxljob執行器,計畫首次啟動不需要註冊執行器
    @return map<jobHander, Cron> 建立xxljob任務時需要的參數配置
    */

    publicvoidloadJar(String path, String fileName, Boolean isRegistXxlJob)throws classNotFoundException, InstantiationException, IllegalAccessException {
    File file = new File(path +"/" + fileName);
    Map<String, String> jobPar = new HashMap<>();
    // 獲取beanFactory
    DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
    // 獲取當前計畫的執行器
    try {
    // URL classloader載入jar包規範必須這麽寫
    URL url = new URL("jar:file:" + file.getAbsolutePath() + "!/");
    URLConnection urlConnection = url.openConnection();
    JarURLConnection jarURLConnection = (JarURLConnection)urlConnection;
    // 獲取jar檔
    JarFile jarFile = jarURLConnection.getJarFile();
    Enumeration<JarEntry> entries = jarFile.entries();
    // 建立自訂類載入器,並加到map中方便管理
    My classLoader my classloader = new My classLoader(new URL[] { url }, classLoader.getSystem classLoader());
    my classLoaderCenter.put(fileName, my classloader);
    Set< class> initBean class = new HashSet<>(jarFile.size());
    // 遍歷檔
    while (entries.hasMoreElements()) {
    JarEntry jarEntry = entries.nextElement();
    if (jarEntry.getName().endsWith(". class")) {
    // 1. 載入類到jvm中
    // 獲取類的全路徑名
    String className = jarEntry.getName().replace('/''.').substring(0, jarEntry.getName().length() - 6);
    // 1.1進行反射獲取
    my classloader.load class( className);
    }
    }
    Map<String, class<?>> loaded classes = my classloader.getLoaded classes();
    XxlJobSpringExecutor xxlJobExecutor = new XxlJobSpringExecutor();
    for(Map.Entry<String, class<?>> entry : loaded classes.entrySet()){
    String className = entry.getKey();
    class<?> clazz = entry.getValue();
    // 2. 將有@spring註解的類交給spring管理
    // 2.1 判斷是否註入spring
    Boolean flag = SpringAnnotationUtils.hasSpringAnnotation(clazz);
    if(flag){
    // 2.2交給spring管理
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
    AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
    // 此處beanName使用全路徑名是為了防止beanName重復
    String packageName = className.substring(0, className.lastIndexOf(".") + 1);
    String beanName = className.substring( className.lastIndexOf(".") + 1);
    beanName = packageName + beanName.substring(01).toLowerCase() + beanName.substring(1);
    // 2.3註冊到spring的beanFactory中
    beanFactory.registerBeanDefinition(beanName, beanDefinition);
    // 2.4允許註入和反向註入
    beanFactory.autowireBean(clazz);
    beanFactory.initializeBean(clazz, beanName);
    /*if(Arrays.stream(clazz.getInterfaces()).collect(Collectors.toSet()).contains(InitializingBean. class)){
    initBean class.add(clazz);
    }*/

    initBean class.add(clazz);
    }
    // 3. 帶有XxlJob註解的方法註冊任務
    // 3.1 過濾方法
    Map<Method, XxlJob> annotatedMethods = null;
    try {
    annotatedMethods = MethodIntrospector.selectMethods(clazz,
    new MethodIntrospector.MetadataLookup<XxlJob>() {
    @Override
    public XxlJob inspect(Method method){
    return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob. class);
    }
    });
    catch (Throwable ex) {
    }
    // 3.2 生成並註冊方法的JobHander
    for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
    Method executeMethod = methodXxlJobEntry.getKey();
    // 獲取jobHander和Cron
    XxlJobCron xxlJobCron = executeMethod.getAnnotation(XxlJobCron. class);
    if(xxlJobCron == null){
    thrownew CustomException("500", executeMethod.getName() + "(),沒有添加@XxlJobCron註解配置定時策略");
    }
    if (!CronExpression.isValidExpression(xxlJobCron.value())) {
    thrownew CustomException("500", executeMethod.getName() + "(),@XxlJobCron參數內容錯誤");
    }
    XxlJob xxlJob = methodXxlJobEntry.getValue();
    jobPar.put(xxlJob.value(), xxlJobCron.value());
    if (isRegistXxlJob) {
    executeMethod.setAccessible(true);
    // regist
    Method initMethod = null;
    Method destroyMethod = null;
    xxlJobExecutor.registJobHandler(xxlJob.value(), new CustomerMethodJobHandler(clazz, executeMethod, initMethod, destroyMethod));
    }
    }
    }
    // spring bean實際註冊
    initBean class.forEach(beanFactory::getBean);
    catch (IOException e) {
    logger.error("讀取{} 檔異常", fileName);
    e.printStackTrace();
    thrownew RuntimeException("讀取jar檔異常: " + fileName);
    }
    }
    }









    以下是判斷該類是否有spring註解的工具類

    apublic  classSpringAnnotationUtils{
    privatestatic Logger logger = LoggerFactory.getLogger(SpringAnnotationUtils. class);
    /**
    * 判斷一個類是否有 Spring 核心註解
    *
    @param clazz 要檢查的類
    @return true 如果該類上添加了相應的 Spring 註解;否則返回 false
    */

    publicstaticbooleanhasSpringAnnotation( class<?> clazz){
    if (clazz == null) {
    returnfalse;
    }
    //是否是介面
    if (clazz.isInterface()) {
    returnfalse;
    }
    //是否是抽象類
    if (Modifier.isAbstract(clazz.getModifiers())) {
    returnfalse;
    }
    try {
    if (clazz.getAnnotation(Component. class) !null ||
    clazz.getAnnotation(Repository. class) !null ||
    clazz.getAnnotation(Service. class) !null ||
    clazz.getAnnotation(Controller. class) !null ||
    clazz.getAnnotation(Configuration. class) !null) {
    returntrue;
    }
    }catch (Exception e){
    logger.error("出現異常:{}",e.getMessage());
    }
    returnfalse;
    }
    }

    註冊xxljob執行器的操作是仿照的xxljob中的XxlJobSpringExecutor的註冊方法。

    3、動態解除安裝

    動態解除安裝的過程,就是將動態載入的程式碼,從記憶體,spring以及xxljob中移除。

    程式碼如下:

    /**
     * 動態解除安裝指定路徑下指定jar包
     * @param fileName
     * @return map<jobHander, Cron> 建立xxljob任務時需要的參數配置
     */

    publicvoidunloadJar(String fileName)throws IllegalAccessException, NoSuchFieldException {
    // 獲取載入當前jar的類載入器
    My classLoader my classLoader = my classLoaderCenter.get(fileName);
    // 獲取jobHandlerRepository私有內容,為了解除安裝xxljob任務
    Field privateField = XxlJobExecutor. class.getDeclaredField("jobHandlerRepository");
    // 設定私有內容可存取
    privateField.setAccessible(true);
    // 獲取私有內容的值jobHandlerRepository
    XxlJobExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    Map<String, IJobHandler> jobHandlerRepository = (ConcurrentHashMap<String, IJobHandler>) privateField.get(xxlJobSpringExecutor);
    // 獲取beanFactory,準備從spring中解除安裝
    DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
    Map<String, class<?>> loaded classes = my classLoader.getLoaded classes();
    Set<String> beanNames = new HashSet<>();
    for (Map.Entry<String, class<?>> entry: loaded classes.entrySet()) {
    // 1. 將xxljob任務從xxljob執行器中移除
    // 1.1 截取beanName
    String key = entry.getKey();
    String packageName = key.substring(0, key.lastIndexOf(".") + 1);
    String beanName = key.substring(key.lastIndexOf(".") + 1);
    beanName = packageName + beanName.substring(01).toLowerCase() + beanName.substring(1);
    // 獲取bean,如果獲取失敗,表名這個類沒有加到spring容器中,則跳出本次迴圈
    Object bean = null;
    try{
    bean = applicationContext.getBean(beanName);
    }catch (Exception e){
    // 異常說明spring中沒有這個bean
    continue;
    }
    // 1.2 過濾方法
    Map<Method, XxlJob> annotatedMethods = null;
    try {
    annotatedMethods = MethodIntrospector.selectMethods(bean.get class(),
    new MethodIntrospector.MetadataLookup<XxlJob>() {
    @Override
    public XxlJob inspect(Method method){
    return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob. class);
    }
    });
    catch (Throwable ex) {
    }
    // 1.3 將job從執行器中移除
    for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
    XxlJob xxlJob = methodXxlJobEntry.getValue();
    jobHandlerRepository.remove(xxlJob.value());
    }
    // 2.0從spring中移除,這裏的移除是僅僅移除的bean,並未移除bean定義
    beanNames.add(beanName);
    beanFactory.destroyBean(beanName, bean);
    }
    // 移除bean定義
    Field mergedBeanDefinitions = beanFactory.get class()
    .getSuper class()
    .getSuper class().getDeclaredField("mergedBeanDefinitions");
    mergedBeanDefinitions.setAccessible(true);
    Map<String, RootBeanDefinition> rootBeanDefinitionMap = ((Map<String, RootBeanDefinition>) mergedBeanDefinitions.get(beanFactory));
    for (String beanName : beanNames) {
    beanFactory.removeBeanDefinition(beanName);
    // 父類bean定義去除
    rootBeanDefinitionMap.remove(beanName);
    }
    // 解除安裝父任務,子任務已經在迴圈中解除安裝
    jobHandlerRepository.remove(fileName);
    // 3.2 從類載入中移除
    try {
    // 從類載入器底層的 classes中移除連線
    Field field = classLoader. class.getDeclaredField(" classes");
    field.setAccessible(true);
    Vector< class<?>> classes = (Vector< class<?>>) field.get(my classLoader);
    classes.removeAllElements();
    // 移除類載入器的參照
    my classLoaderCenter.remove(fileName);
    // 解除安裝類載入器
    my classLoader.unload();
    catch (NoSuchFieldException e) {
    logger.error("動態解除安裝的類,從類載入器中解除安裝失敗");
    e.printStackTrace();
    catch (IllegalAccessException e) {
    logger.error("動態解除安裝的類,從類載入器中解除安裝失敗");
    e.printStackTrace();
    }
    logger.error("{} 動態解除安裝成功", fileName);
    }




    4、動態配置

    使用動態載入時,為了避免服務重新啟動後遺失已載入的任務包,使用動態配置的方式,載入後動態更新初始化載入配置。

    以下提供了兩種自己實際操作過的配置方式。

    4.1 動態修改本地yml

    動態修改本地yml配置檔,需要添加snakeyaml的依賴

    4.1.1 依賴引入

    <dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.29</version>
    </dependency>

    4.1.2 工具類

    讀取指定路徑下的配置檔,並進行修改。

    package com.jy.util;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.env.Environment;
    import org.springframework.stereotype.Component;
    import org.yaml.snakeyaml.DumperOptions;
    import org.yaml.snakeyaml.Yaml;
    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    /**
     * 用於動態修改bootstrap.yml配置檔
     * @author lijianyu
     * @date 2023/04/18 17:57
     **/

    @Component
    public classConfigUpdater{
    publicvoidupdateLoadJars(List<String> jarNames)throws IOException {
    // 讀取bootstrap.yml
    Yaml yaml = new Yaml();
    InputStream inputStream = new FileInputStream(new File("src/main/resources/bootstrap.yml"));
    Map<String, Object> obj = yaml.load(inputStream);
    inputStream.close();
    obj.put("loadjars", jarNames);
    // 修改
    FileWriter writer = new FileWriter(new File("src/main/resources/bootstrap.yml"));
    DumperOptions options = new DumperOptions();
    options.setDefaultFlow style(DumperOptions.Flow style.BLOCK);
    options.setPrettyFlow(true);
    Yaml yamlWriter = new Yaml(options);
    yamlWriter.dump(obj, writer);
    }
    }




    4.2 動態修改 nacos 配置

    Spring Cloud Alibaba Nacos元件完全支持在執行時透過程式碼動態修改配置,還提供了一些API供開發者在程式碼裏面實作動態修改配置。在每次動態載入或解除安裝數據治理任務jar包時,執行成功後都會進行動態更新nacos配置。

    package cn.jy.sjzl.config;
    import com.alibaba.nacos.api.NacosFactory;
    import com.alibaba.nacos.api.config.ConfigService;
    import com.alibaba.nacos.api.exception.NacosException;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Configuration;
    import java.util.Properties;
    @Configuration
    public classNacosConfig{
    @Value("${spring.cloud.nacos.server-addr}")
    private String serverAddr;
    @Value("${spring.cloud.nacos.config.namespace}")
    private String namespace;
    public ConfigService configService()throws NacosException {
    Properties properties = new Properties();
    properties.put("serverAddr", serverAddr);
    properties.put("namespace", namespace);
    return NacosFactory.createConfigService(properties);
    }
    }



    package cn.jy.sjzl.util;
    import cn.jy.sjzl.config.NacosConfig;
    import com.alibaba.fastjson.JSONObject;
    import com.alibaba.nacos.api.config.ConfigService;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;
    /**
     * nacos配置中,修改sjzl-loadjars.yml
     *
     * @author lijianyu
     * @date 2023/04/19 17:59
     **/

    @Component
    public classNacosConfigUtil{
    privatestatic Logger logger = LoggerFactory.getLogger(NacosConfigUtil. class);
    @Autowired
    private NacosConfig nacosConfig;
    private String dataId = "sjzl-loadjars.yml";
    @Value("${spring.cloud.nacos.config.group}")
    private String group;
    /**
    * 從nacos配置檔中,添加初始化jar包配置
    @param jarName 要移除的jar包名
    @throws Exception
    */

    publicvoidaddJarName(String jarName)throws Exception {
    ConfigService configService = nacosConfig.configService();
    String content = configService.getConfig(dataId, group, 5000);
    // 修改配置檔內容
    YAMLMapper yamlMapper = new YAMLMapper();
    ObjectMapper jsonMapper = new ObjectMapper();
    Object yamlObject = yamlMapper.readValue(content, Object. class);
    String jsonString = jsonMapper.writeValueAsString(yamlObject);
    JSONObject jsonObject = JSONObject.parseObject(jsonString);
    List<String> loadjars;
    if (jsonObject.containsKey("loadjars")) {
    loadjars = (List<String>) jsonObject.get("loadjars");
    }else{
    loadjars = new ArrayList<>();
    }
    if (!loadjars.contains(jarName)) {
    loadjars.add(jarName);
    }
    jsonObject.put("loadjars" , loadjars);
    Object yaml = yamlMapper.readValue(jsonMapper.writeValueAsString(jsonObject), Object. class);
    String newYamlString = yamlMapper.writeValueAsString(yaml);
    boolean b = configService.publishConfig(dataId, group, newYamlString);
    if(b){
    logger.info("nacos配置更新成功");
    }else{
    logger.info("nacos配置更新失敗");
    }
    }
    }









    三、分離打包

    分離打包時,根據實際情況在pom.xml中修改以下配置

    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.4</version>
    <executions>
    <execution>
    <phase>package</phase>
    <goals>
    <goal>shade</goal>
    </goals>
    <configuration>
    <filters>
    <filter>
    <artifact>*:*</artifact>
    <includes>
    <include>com/jy/job/demo/**</include>
    </includes>
    </filter>
    </filters>
    <finalName>demoJob</finalName>
    </configuration>
    </execution>
    </executions>
    </plugin>
    </plugins>
    </build>

    你還有什麽想要補充的嗎?

    最後,再次推薦下我們的AI星

    為了跟上AI時代我幹了一件事兒,我建立了一個知識星球社群:ChartGPT與副業。想帶著大家一起探索 ChatGPT和新的AI時代

    有很多小夥伴搞不定ChatGPT帳號,於是我們決定,凡是這三天之內加入ChatPGT的小夥伴,我們直接送一個正常可用的永久ChatGPT獨立帳戶。

    不光是增長速度最快,我們的星球品質也絕對經得起考驗,短短一個月時間,我們的課程團隊釋出了 8個專欄、18個副業計畫

    簡單說下這個星球能給大家提供什麽:

    1、不斷分享如何使用ChatGPT來完成各種任務,讓你更高效地使用ChatGPT,以及副業思考、變現思路、創業案例、落地案例分享。

    2、分享ChatGPT的使用方法、最新資訊、商業價值。

    3、探討未來關於ChatGPT的機遇,共同成長。

    4、幫助大家解決ChatGPT遇到的問題。

    5、 提供一整年的售後服務,一起搞副業

    星球福利:

    1、加入星球4天後,就送ChatGPT獨立帳號。

    2、邀請你加入ChatGPT會員交流群。

    3、贈送一份完整的ChatGPT手冊和66個ChatGPT副業賺錢手冊。

    其它福利還在籌劃中... 不過,我給你大家保證,加入星球後,收獲的價值會遠遠大於今天加入的門票費用 !

    本星球第一期原價 399 ,目前屬於試營運,早鳥價 139 ,每超過50人漲價10元,星球馬上要來一波大的漲價,如果你還在猶豫,可能最後就要以 更高價格加入了 。。

    早就是優勢。 建議大家盡早以便宜的價格加入!

    歡迎有需要的同學試試,如果本文對您有幫助,也請幫忙點個 贊 + 在看 啦!❤️

    在 還有更多優質計畫系統學習資源,歡迎分享給其他同學吧!

    PS:如果覺得我的分享不錯,歡迎大家隨手點贊、轉發、在看。

    最後給讀者整理了一份BAT大廠面試真題,需要的可掃碼加微信備註:「面試」獲取。

    版權申明:內容來源網路,版權歸原創者所有。除非無法確認,我們都會標明作者及出處,如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    END

    最近面試BAT,整理一份面試資料【Java面試BAT通關手冊】,覆蓋了Java核心技術、JVM、Java並行、SSM、微服務、資料庫、數據結構等等。在這裏,我為大家準備了一份2021年最新最全BAT等大廠Java面試經驗總結。

    別找了,想獲取史上最全的Java大廠面試題學習資料

    掃下方二維碼回復面試就好了

    歷史好文:

    掃碼關註後端架構師」,選擇星標公眾號

    重磅幹貨,第一時間送達

    ,你在看嗎?