前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试官问我咋实现Spring框架IOC和DI好吧打趴下,深度解析手动实现Spring框架的IOC与DI功能

面试官问我咋实现Spring框架IOC和DI好吧打趴下,深度解析手动实现Spring框架的IOC与DI功能

原创
作者头像
杨不易呀
发布2023-09-26 00:26:36
4030
发布2023-09-26 00:26:36
举报
文章被收录于专栏:杨不易呀

前言

如果还不会 Spring源码编译 可去看看 Spring源码编译

面试官问我咋实现Spring框架IOC和DI好吧打趴下,深度解析手动实现Spring框架的IOC与DI功能

工程搭建

使用 Maven 创建普通 Web 工程:

image-20210929173602607
image-20210929173602607
image-20210929173730234
image-20210929173730234

修改 pom.xml 添加依赖内容如下图:

image-20210929174554931
image-20210929174554931
代码语言:html
复制
<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.28</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
    </dependency>
</dependencies>

配置 Tomcat

image-20210929185718723
image-20210929185718723
image-20210929190435458
image-20210929190435458
image-20210929190535606
image-20210929190535606

添加 Tomcat 的 jar 包:

image-20210929190927634
image-20210929190927634
image-20210929191018877
image-20210929191018877
image-20210929191132686
image-20210929191132686
image-20210929191155985
image-20210929191155985
image-20210929191212712
image-20210929191212712

点击上图中的 Add Selected 即可完成添加,添加完成了之后你在创建的时候就会出现 Servlet 选项如下图所示:

image-20210929191424530
image-20210929191424530

创建一个 Servlet 测试一下:

代码语言:java
复制
/**
 * 控制器
 *
 * @author yby6
 * @date 2023/09/29
 */
@WebServlet(name = "UserServlet", value = "/UserServlet")
public class UserServlet extends HttpServlet {
    @Override
    public void service(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("test Servlet");
    }
}

启动 Tomcat 在浏览器中访问:http://127.0.0.1:8080/UserServlet 即可测试我的测试结果如下图所示:

image-20210929192250080
image-20210929192250080

好了到了这里咱们的工程就已经创建完毕了,回归到我们文章的主要内容。

整体思路

image-20210929192718391
image-20210929192718391

解析配置

将相关配置加载进内存当中,存入定义好的数据结构,要管理哪些目标对象,通过编程语言进行解析,配置可以使用 xml,properties 或注解的方式。

定位与注册对象

查看哪些类当中标记了注解,定位到了目标对象之后,将对象注册到容器当中管理起来。

注入对象

在用户需要用到对象时,从容器中精确的获取对象,返回给用户给对应的属性进行注入(赋值)

提供通用的工具类

通过封装一些通用的工具,能够方便框架或用户方便进行操作。

image-20210930085047817
image-20210930085047817

创建注解

分别创建如下几个注解:

image-20210930091728255
image-20210930091728255

Component.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/9/30 030 9:07
 * @description
 **/
// 作用在类上
@Target(ElementType.TYPE)
// 注解的生命周期为RUNTIME,因为使用反射创建对象
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}

Controller.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/9/30 030 9:07
 * @description
 **/
// 作用在类上
@Target(ElementType.TYPE)
// 注解的生命周期为RUNTIME,因为使用反射创建对象
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}

Repository.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/9/30 030 9:07
 * @description
 **/
// 作用在类上
@Target(ElementType.TYPE)
// 注解的生命周期为RUNTIME,因为使用反射创建对象
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
}

Service.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/9/30 030 9:07
 * @description
 **/
// 作用在类上
@Target(ElementType.TYPE)
// 注解的生命周期为RUNTIME,因为使用反射创建对象
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}

获取指定包下所有的类

指定范围,获取范围内的所有类,遍历所有类,获取被注解标记的类并加载进容器里,使用 classLoad 获取资源路径,直接使用 top.it6666 包是无法定位到对应的文件夹所在的路径,必须拿到具体路径,才能获取到该路径下所有 .class 文件。

ClassLoad 的作用:根据一个指定的类的名称,找到或者生成其对应的字节码,加载 Java 应用所需的资源:图片文件、配置文件、文件目录。

image-20210930170408049
image-20210930170408049

获取 URL 中文件与目录

image-20210930170558310
image-20210930170558310

根据文件与目录提取所有的 .class 文件

image-20210930170852220
image-20210930170852220

最终 ClassUtil.java 的内容如下:

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/9/30 030 9:24
 * @description 获取指定包下所有的类
 **/
@Slf4j
@UtilityClass
public class ClassUtil {
    public static final String FILE_PROTOCOL = "file";
    public static final String CLASS_SUFFIX = ".class";

    /**
     * 获取指定包下所有的类
     *
     * @param packageName 包名
     * @return {@link Set}<{@link Class}<{@link ?}>>
     */
    @SuppressWarnings("all")
    public Set<Class<?>> getPackageClass(String packageName) {
        // 1.获取类加载器
        ClassLoader classLoader = ClassUtil.getClassLoad();

        // 2.通过类加载器获取到加载的资源
        // 2.1把top.it6666转成top/it6666
        // 2.2通过类加载器获取加载的资源
        URL url = classLoader.getResource(packageName.replace(".", "/"));
        if (null == url) {
            log.error("{}:该包下没有任何内容", packageName);
        }

        Set<Class<?>> classSet = null;

        // 判断url是否为文件
        if (Objects.equals(FILE_PROTOCOL, url.getProtocol())) {
            // 创建集合
            classSet = new HashSet<>();

            String path = url.getPath();
            try {
                path = URLDecoder.decode(path, "utf-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }

            // 获取资源的实际路径
            File packageDir = new File(path);
            // 从路径当中提取Class文件,放到Set集合
            ClassUtil.getDirClass(classSet, packageDir, packageName);
        }

        return classSet;
    }

    ClassLoader getClassLoad() {
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * 根据目录提出所有的class文件
     *
     * @param classSet    Ioc容器
     * @param packageDir  文件
     * @param packageName 包名
     */
    private static void getDirClass(Set<Class<?>> classSet, File packageDir, String packageName) {
        // 如果不是一个目录直接结束
        if (!packageDir.isDirectory()) {
            return;
        }

        // 如果是目录,对目录里面的内容进行过滤
        File[] files = packageDir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
                if (file.isDirectory()) {
                    return true;
                }
                String absolutePath = file.getAbsolutePath();

                // 判断absolutePath是不是以 .class 后缀结尾
                if (absolutePath.endsWith(CLASS_SUFFIX)) {
                    // 到了这里就代表是 .class 结尾的
                    addToClassSet(absolutePath);
                }
                return false;
            }

            private void addToClassSet(String absolutePath) {
                // absolutePath 有可能是:/D:/aa/aaa 或者是:D:\bb\bb 可以使用 File.separator 来统一代替
                // 把文件目录转成包的形式
                absolutePath = absolutePath.replace(File.separator, ".");

                String className = absolutePath.substring(absolutePath.indexOf(packageName));
                className = className.substring(0, className.lastIndexOf("."));

                try {
                    Class<?> targetClass = Class.forName(className);
                    classSet.add(targetClass);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        });

        if (null != files) {
            Arrays.stream(files).forEach(childFile -> ClassUtil.getDirClass(classSet, childFile, packageName));
        }
    }
}

创建 BeanContainer 容器

BeanContainer.java

image-20211009092336221
image-20211009092336221
代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/10/08 008 17:24
 * @description
 **/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("unused")
public class BeanContainer {

    public static BeanContainer getInstance() {
        return ContainerHolder.HOLDER.instance;
    }

    /**
     * 容器枚举
     *
     * @author yby6
     * @date 2023/10/08
     */
    private enum ContainerHolder {
        /**
         * 持有人
         */
        HOLDER;
        /**
         * 实例
         */
        private final BeanContainer instance;

        /**
         * 容器
         */
        ContainerHolder() {
            instance = new BeanContainer();
        }
    }
}

定义相关属性扫描所有 Bean

image-20211009104620746
image-20211009104620746
image-20211009104640430
image-20211009104640430
image-20211009104655261
image-20211009104655261

修改 BeanContainer.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/10/08 008 17:24
 * @description
 **/
@Slf4j
@SuppressWarnings("unused")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BeanContainer {

    public static BeanContainer getInstance() {
        return ContainerHolder.HOLDER.instance;
    }

    /**
     * 容器枚举
     *
     * @author yby6
     * @date 2023/10/08
     */
    private enum ContainerHolder {
        /**
         * 持有人
         */
        HOLDER;
        /**
         * 实例
         */
        private final BeanContainer instance;

        /**
         * 容器
         */
        ContainerHolder() {
            instance = new BeanContainer();
        }
    }

    /**
     * bean注释
     */
    private final List<Class<? extends Annotation>> beanAnnotation =
            Arrays.asList(Component.class, Controller.class, Service.class, Repository.class);

    /**
     * bean映射
     */
    private final Map<Class<?>, Object> beanMap = new ConcurrentHashMap<>();

    /**
     * 容器实例的个数
     *
     * @return int
     */
    public int size() {
        return beanMap.size();
    }

    /**
     * 加载状态
     */
    private boolean loaded = false;

    /**
     * 容器是否被加载过
     *
     * @return boolean
     */
    public boolean isLoaded() {
        return loaded;
    }

    /**
     * 加载bean
     *
     * @param packageName 包名
     */
    public synchronized void loadBeans(String packageName) {
        // 判断容器是否被加载过
        if (isLoaded()) {
            log.warn("容器已经被加载!");
            return;
        }

        // 1.获取该包下所有的字节码
        Set<Class<?>> packageClass = ClassUtil.getPackageClass(packageName);

        if (null == packageClass || packageClass.isEmpty()) {
            log.warn("get null from packageName:{}", packageName);
            return;
        }

        // 2.遍历所有的字节码对象
        packageClass.forEach(clazz -> beanAnnotation.forEach(annotation -> {
            // 2.1.判断当前字节码上是否有指定的注解
            if (clazz.isAnnotationPresent(annotation)) {
                // 创建对象存放到集合当中
                beanMap.put(clazz, BeanContainer.newInstance(clazz, false));
            }
        }));
        loaded = true;
    }

    private static <T> T newInstance(Class<?> clazz, boolean accessible) {
        Constructor<?> declaredConstructor;
        try {
            declaredConstructor = clazz.getDeclaredConstructor();
            declaredConstructor.setAccessible(accessible);
            return (T) declaredConstructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("创建实例失败!");
        }
    }
}

改造一下我们之前的测试类代码移动目录位置,移动到 test 包当中如下,顺便改一下我们自定义的 IOC 与 DI 功能代码存放包的位置:

image-20211018114443166
image-20211018114443166
image-20211018111657911
image-20211018111657911
代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/9/30 030 14:36
 * @description
 **/
public class MyTest {

    private static BeanContainer beanContainer;

    @BeforeAll
    public static void init() {
        beanContainer = BeanContainer.getInstance();
    }

    @Test
    @SuppressWarnings("all")
    void testClassUtil() {
        ClassUtil.getPackageClass("top.it6666").forEach(System.out::println);
    }

    @Test
    void testBeanContainer() {
        Assertions.assertFalse(beanContainer.isLoaded());
        beanContainer.loadBeans("top.it6666");
        Assertions.assertEquals(1, beanContainer.size());
        Assertions.assertTrue(beanContainer.isLoaded());
    }
}

创建一个 MyController.java 然后在该类上添加 @Controller 注解导包是我们自己定义的那个注解:

image-20211018112042099
image-20211018112042099

然后运行测试类即可查看结果:

image-20211018112109660
image-20211018112109660

容器相关操作

增加删除操作,可以通过 class 获取对应的实例。获取所有的 key 和所有的 value。通过注解来获取被注解标注的 class。通过传入接口,来获取接口实现类的 class 对象。

image-20211022140640250
image-20211022140640250
代码语言:java
复制
/**
 * 添加bean
 *
 * @param clazz clazz
 * @param obj   obj
 * @return {@link Object}
 */
public Object addBean(Class<?> clazz, Object obj) {
    return beanMap.put(clazz, obj);
}

/**
 * 删除bean
 *
 * @param clazz clazz
 * @return {@link Object}
 */
public Object removeBean(Class<?> clazz) {
    return beanMap.remove(clazz);
}

/**
 * 获取Bean
 *
 * @param clazz clazz
 * @return {@link Object}
 */
public Object getBean(Class<?> clazz) {
    return beanMap.get(clazz);
}

/**
 * 获取容器所有的classes
 *
 * @return {@link Set}<{@link Class}<{@link ?}>>
 */
public Set<Class<?>> getClasses() {
    return beanMap.keySet();
}

/**
 * 获取容器所有的Bean实例
 *
 * @return {@link Set}<{@link Object}>
 */
public Set<Object> getBeans() {
    return new HashSet<>(beanMap.values());
}

添加根据指定注解获取该注解标记的 Bean 的 Class 集合:

代码语言:java
复制
/**
 * 根据指定注解获取该注解标记的 Bean 的 Class 集合
 *
 * @param annotation 注解
 * @return {@link Set}<{@link Class}<{@link ?}>>
 */
public Set<Class<?>> getClassesByAnnotation(Class<? extends Annotation> annotation) {
    // 1.获取容器当中所有的class
    Set<Class<?>> classes = this.getClasses();
    if (null == classes || classes.isEmpty()) {
        log.info("没有任何信息!");
        return null;
    }
    
    Set<Class<?>> classSet = new HashSet<>();
    classes.forEach(clazz -> {
        if (clazz.isAnnotationPresent(annotation)) {
            // 保存到集合当中
            classSet.add(clazz);
        }
    });
    return classSet.size() > 0 ? classSet : null;
}

添加根据传入的接口,到容器当中找对应的实现类或子类:

代码语言:java
复制
/**
 * 根据传入的接口,到容器当中找对应的实现类或子类
 *
 * @param interfaceOrClass 接口或类
 * @return {@link Set}<{@link Class}<{@link ?}>>
 */
public Set<Class<?>> getClassBySuper(Class<?> interfaceOrClass) {
    // 1.获取容器当中所有的class
    Set<Class<?>> classes = getClasses();
    if (null == classes || classes.isEmpty()) {
        log.info("没有任何信息!");
        return null;
    }

    Set<Class<?>> classSet = new HashSet<>();
    classes.forEach(clazz -> {
        // 判断当前字节码是否为指定字节码的子类或接口并且指定的字节码不等于本身字节码
        if (interfaceOrClass.isAssignableFrom(clazz) && !Objects.equals(clazz, interfaceOrClass)) {
            classSet.add(clazz);
        }
    });

    return classSet.size() > 0 ? classSet : null;
}

依赖注入

定义相关的注解标签,实现创建被注解标记的成员变量实例,并将其注入到成员变量里,依赖注入使用:

创建 Autowired.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/10/25 025 9:01
 * @description
 **/
/*
及作用在成员变量上
* */
@Target(ElementType.FIELD)
/*
保留在运行时
* */
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
    String value() default "";
}

依赖注入实现思路:

image-20211025090534124
image-20211025090534124

依赖注入整体实现

DependencyInjector.java

代码语言:java
复制
/**
 * 依赖注入
 *
 * @author yby6
 * @date 2023/10/25
 */
@Slf4j
public class DependencyInjector {
    /**
     * Bean容器
     */
    private final BeanContainer beanContainer;

    /**
     * 依赖注入,构造器
     */
    public DependencyInjector() {
        beanContainer = BeanContainer.getInstance();
    }

    /**
     * 执行Ioc操作
     */
    public void doIoc() {
        // 1.遍历Bean容器中所有的Class对象
        beanContainer.getClasses().forEach(clazz -> {
            // 2.遍历Class对象的所有成员变量
            Field[] declaredFields = clazz.getDeclaredFields();

            if (declaredFields.length == 0) {
                // 当前没有成员变量,继续查找下一个
                return;
            }

            // 有成员变量
            Arrays.stream(declaredFields).forEach(field -> {
                // 3.找出被Autowired标记的成员变量
                if (field.isAnnotationPresent(Autowired.class)) {
                    // 获取注解实例
                    // 被 Autowired注解标注
                    // 4.获取这些成员变量的类型
                    Class<?> fieldType = field.getType();

                    // 5.获取成员变量的类型在容器里对应的实例
                    Object instanceObj = this.getFieldInstance(fieldType, field.getAnnotation(Autowired.class).value());

                    // 6.通过反射将对象的成员变量实例进行赋值操作
                    if (null == instanceObj) {
                        throw new RuntimeException("成员属性注入失败:" + fieldType.getName());
                    } else {
                        // 获取当前类实例
                        // 给当前类实例成员变量赋值
                        this.setField(field, beanContainer.getBean(clazz), instanceObj, true);
                    }
                }
            });
        });
    }

    /**
     * 根据Class在BeanContainer里获取其实例或者实现类
     *
     * @param fieldClass     成员变量类型
     * @param autowiredValue autowired注解的内容
     * @return {@link Object}
     */
    private Object getFieldInstance(Class<?> fieldClass, String autowiredValue) {
        Object fieldValue = beanContainer.getBean(fieldClass);
        // 字段是实现类,直接返回
        if (null != fieldValue) {
            return fieldValue;
        } else {
            // 字段是接口
            Class<?> implClass = this.getImplClass(fieldClass, autowiredValue);
            if (implClass != null) {
                return beanContainer.getBean(implClass);
            } else {
                return null;
            }
        }
    }

    /**
     * 得到impl类
     *
     * @param fieldClass     接口
     * @param autowiredValue autowired的价值
     * @return {@link Class}<{@link ?}>
     */
    private Class<?> getImplClass(Class<?> fieldClass, String autowiredValue) {
        //从容器当中获取该接口的实现类
        Set<Class<?>> classSet = beanContainer.getClassBySuper(fieldClass);
        if (null != classSet && !classSet.isEmpty()) {
            if (Objects.equals(autowiredValue, "")) {
                if (classSet.size() == 1) {
                    return classSet.iterator().next();
                } else {
                    // 如果有多个实现类
                    throw new RuntimeException("有多个实现类,请指定具体的实现类名称:" + fieldClass.getName());
                }
            } else {
                // 注解当中有设置值
                for (Class<?> clazz : classSet) {
                    if (autowiredValue.equals(clazz.getSimpleName())) {
                        return clazz;
                    }
                }
            }
        }
        return null;
    }

    /**
     * 设置字段
     *
     * @param field        字段
     * @param targetObject 目标对象
     * @param obj          obj
     * @param accessible   是否可访问
     */
    private void setField(Field field, Object targetObject, Object obj, boolean accessible) {
        field.setAccessible(accessible);
        try {
            field.set(targetObject, obj);
        } catch (IllegalAccessException e) {
            log.error("setField error:" + e);
            throw new RuntimeException(e);
        }
    }
}

测试依赖注入

创建 Service

IUserService.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/10/25 025 10:03
 * @description
 **/
public interface IUserService {
    /**
     * 显示
     */
    void show();
}

IUserServiceImpl.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/10/25 025 10:04
 * @description
 **/
@Service
public class IUserServiceImpl implements IUserService {
    @Override
    public void show() {
        System.out.println("my is IUserServiceImpl");
    }
}
image-20211025103605008
image-20211025103605008

修改 MyController.java

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/10/09 009 11:31
 * @description
 **/
@Controller
public class MyController {

    @Autowired
    private IUserService iUserService;

    public void testShow() {
        this.iUserService.show();
    }
}

修改 MyTest.java 当前 IUserService 只有一个实现类所以 @Autowired 是可以直接标记即可不用指定名称,在 MyTest 添加 testIoc 方法:

代码语言:java
复制
@Test
void testIoc() {
    beanContainer.loadBeans("top.it6666");
    MyController myController = (MyController) beanContainer.getBean(MyController.class);

    DependencyInjector dependencyInjector = new DependencyInjector();
    dependencyInjector.doIoc();

    myController.testShow();
}

运行结果如下:

image-20211025103901930
image-20211025103901930

如上是只有一个实现类,紧接着在来新建一个实现类,在 impl 包当中创建 ITaoBaoServiceImpl.java:

代码语言:java
复制
/**
 * @author yby6
 * @program SpringPro
 * @date Created in 2023/10/25 025 10:42
 * @description
 **/
@Service
public class ITaoBaoServiceImpl implements IUserService {
    @Override
    public void show() {
        System.out.println("my is ITaoBaoServiceImpl");
    }
}

再次运行测试类代码结果如下图:

image-20211025104442827
image-20211025104442827

修改 MyController.java:

image-20211025104519218
image-20211025104519218

再次运行测试类代码运行结果如下图所示:

image-20211025104620898
image-20211025104620898

最后

本期结束咱们下次再见👋~

点赞
点赞

🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 工程搭建
  • 配置 Tomcat
  • 整体思路
    • 解析配置
      • 定位与注册对象
        • 注入对象
          • 提供通用的工具类
          • 创建注解
          • 获取指定包下所有的类
          • 获取 URL 中文件与目录
          • 根据文件与目录提取所有的 .class 文件
          • 创建 BeanContainer 容器
          • 定义相关属性扫描所有 Bean
          • 容器相关操作
          • 依赖注入
            • 依赖注入整体实现
            • 测试依赖注入
            • 最后
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档