当前位置: 欣欣网 > 码农

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大厂面试题学习资料

    扫下方二维码回复面试就好了

    历史好文:

    扫码关注后端架构师」,选择星标公众号

    重磅干货,第一时间送达

    ,你在看吗?