关于java:面试官展开说说Spring中Bean对象是如何通过注解注入的

49次阅读

共计 14542 个字符,预计需要花费 37 分钟才能阅读完成。

作者:小傅哥
博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!😄

章节目录(手写 Spring,让你理解更多)

  • [x] 第 01 章:开篇介绍,我要带你撸 Spring 啦!
  • [x] 第 02 章:小试牛刀,实现一个简略的 Bean 容器
  • [x] 第 03 章:初显身手,使用设计模式,实现 Bean 的定义、注册、获取
  • [x] 第 04 章:锋芒毕露,基于 Cglib 实现含构造函数的类实例化策略
  • [x] 第 05 章:一举成名,为 Bean 对象注入属性和依赖 Bean 的性能实现
  • [x] 第 06 章:气吞山河,设计与实现资源加载器,从 Spring.xml 解析和注册 Bean 对象
  • [x] 第 07 章:所向无敌,实现利用上下文,自动识别、资源加载、扩大机制
  • [x] 第 08 章:龙行有风,向虚拟机注册钩子,实现 Bean 对象的初始化和销毁办法
  • [x] 第 09 章:虎行有雨,定义标记类型 Aware 接口,实现感知容器对象
  • [x] 第 10 章:横刀跃马,对于 Bean 对象作用域以及 FactoryBean 的实现和应用
  • [x] 第 11 章:更上层楼,基于观察者实现,容器事件和事件监听器
  • [x] 第 12 章:炉火纯青,基于 JDK 和 Cglib 动静代理,实现 AOP 外围性能
  • [x] 第 13 章:行云流水,把 AOP 动静代理,融入到 Bean 的生命周期
  • [x] 第 14 章:笑傲江湖,通过注解配置和包主动扫描的形式实现 Bean 对象的注册
  • [x] 第 15 章:万人之敌,通过注解给属性注入配置和 Bean 对象
  • [] 第 16 章:待归档 …

一、前言

写代码,就是从能用到好用的一直折腾!

你听过扰动函数吗?你写过斐波那契(Fibonacci)散列吗?你实现过梅森旋转算法吗?怎么 没听过这些写不了代码吗!不会的,即便没听过你一样能够写的了代码,比方你实现的数据库路由数据总是落在 1 库 1 表它不散列散布、你实现的抽奖零碎总是把经营配置的最大红包收回去进步了经营老本、你开发的秒杀零碎总是在开始后的 1 秒就挂了货品基本给不进来。

除了一部分仅把编码当成搬砖应酬工作外的程序员,还有一部分总是在谋求极致的码农。写代码还能赚钱,真开心! 这样的码农总是会思考🤔还有没有更好的实现逻辑能让代码不仅是能用,还要好用呢?其实这一点的谋求到实现,须要大量扩展性学习和深度开掘,这样你设计进去的零碎才更你思考的更加全面,也能应答各种简单的场景。

二、指标

在目前 IOC、AOP 两大外围功能模块的撑持下,齐全能够治理 Bean 对象的注册和获取,不过这样的应用形式总感觉像是刀耕火种有点难用。因而在上一章节咱们解决须要手动配置 Bean 对象到 spring.xml 文件中,改为能够主动扫描带有注解 @Component 的对象实现主动拆卸和注册到 Spring 容器的操作。

那么在主动扫描包注册 Bean 对象之后,就须要把原来在配置文件中通过 property name="token" 配置属性和 Bean 的操作,也改为能够主动注入。这就像咱们应用 Spring 框架中 @Autowired@Value 注解一样,实现咱们对属性和对象的注入操作。

三、计划

其实从咱们在实现 Bean 对象的根底性能后,后续陆续增加的性能都是围绕着 Bean 的生命周期进行的,比方批改 Bean 的定义 BeanFactoryPostProcessor,解决 Bean 的属性要用到 BeanPostProcessor,实现共性的属性操作则专门继承 BeanPostProcessor 提供新的接口,因为这样能力通过 instanceof 判断出具备标记性的接口。所以对于 Bean 等等的操作,以及监听 Aware、获取 BeanFactory,都须要在 Bean 的生命周期中实现。那么咱们在设计属性和 Bean 对象的注入时候,也会用到 BeanPostProcessor 来实现在设置 Bean 属性之前,容许 BeanPostProcessor 批改属性值。整体设计构造如下图:

  • 要解决主动扫描注入,包含属性注入、对象注入,则须要在对象属性 applyPropertyValues 填充之前,把属性信息写入到 PropertyValues 的汇合中去。这一步的操作相当于是解决了以前在 spring.xml 配置属性的过程。
  • 而在属性的读取中,须要依赖于对 Bean 对象的类中属性的配置了注解的扫描,field.getAnnotation(Value.class); 顺次拿出合乎的属性并填充上相应的配置信息。这里有一点,属性的配置信息须要依赖于 BeanFactoryPostProcessor 的实现类 PropertyPlaceholderConfigurer,把值写入到 AbstractBeanFactory 的 embeddedValueResolvers 汇合中,这样能力在属性填充中利用 beanFactory 获取相应的属性值
  • 还有一个是对于 @Autowired 对于对象的注入,其实这一个和属性注入的惟一区别是对于对象的获取 beanFactory.getBean(fieldType),其余就没有什么差一点了。
  • 当所有的属性被设置到 PropertyValues 实现当前,接下来就到了创建对象的下一步,属性填充,而此时就会把咱们一一获取到的配置和对象填充到属性上,也就实现了主动注入的性能。

四、实现

1. 工程构造

small-spring-step-14
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework
    │           ├── aop
    │           │   ├── aspectj
    │           │   │   └── AspectJExpressionPointcut.java
    │           │   │   └── AspectJExpressionPointcutAdvisor.java
    │           │   ├── framework 
    │           │   │   ├── adapter
    │           │   │   │   └── MethodBeforeAdviceInterceptor.java
    │           │   │   ├── autoproxy
    │           │   │   │   └── MethodBeforeAdviceInterceptor.java
    │           │   │   ├── AopProxy.java
    │           │   │   ├── Cglib2AopProxy.java
    │           │   │   ├── JdkDynamicAopProxy.java
    │           │   │   ├── ProxyFactory.java
    │           │   │   └── ReflectiveMethodInvocation.java
    │           │   ├── AdvisedSupport.java
    │           │   ├── Advisor.java
    │           │   ├── BeforeAdvice.java
    │           │   ├── ClassFilter.java
    │           │   ├── MethodBeforeAdvice.java
    │           │   ├── MethodMatcher.java
    │           │   ├── Pointcut.java
    │           │   ├── PointcutAdvisor.java
    │           │   └── TargetSource.java
    │           ├── beans
    │           │   ├── factory  
    │           │   │   ├── annotation
    │           │   │   │   ├── Autowired.java
    │           │   │   │   ├── AutowiredAnnotationBeanPostProcessor.java
    │           │   │   │   ├── Qualifier.java
    │           │   │   │   └── Value.java
    │           │   │   ├── config
    │           │   │   │   ├── AutowireCapableBeanFactory.java
    │           │   │   │   ├── BeanDefinition.java
    │           │   │   │   ├── BeanFactoryPostProcessor.java
    │           │   │   │   ├── BeanPostProcessor.java
    │           │   │   │   ├── BeanReference.java
    │           │   │   │   ├── ConfigurableBeanFactory.java
    │           │   │   │   ├── InstantiationAwareBeanPostProcessor.java
    │           │   │   │   └── SingletonBeanRegistry.java
    │           │   │   ├── support
    │           │   │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   │   ├── AbstractBeanDefinitionReader.java
    │           │   │   │   ├── AbstractBeanFactory.java
    │           │   │   │   ├── BeanDefinitionReader.java
    │           │   │   │   ├── BeanDefinitionRegistry.java
    │           │   │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   │   ├── DefaultListableBeanFactory.java
    │           │   │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   │   ├── DisposableBeanAdapter.java
    │           │   │   │   ├── FactoryBeanRegistrySupport.java
    │           │   │   │   ├── InstantiationStrategy.java
    │           │   │   │   └── SimpleInstantiationStrategy.java  
    │           │   │   ├── support
    │           │   │   │   └── XmlBeanDefinitionReader.java
    │           │   │   ├── Aware.java
    │           │   │   ├── BeanClassLoaderAware.java
    │           │   │   ├── BeanFactory.java
    │           │   │   ├── BeanFactoryAware.java
    │           │   │   ├── BeanNameAware.java
    │           │   │   ├── ConfigurableListableBeanFactory.java
    │           │   │   ├── DisposableBean.java
    │           │   │   ├── FactoryBean.java
    │           │   │   ├── HierarchicalBeanFactory.java
    │           │   │   ├── InitializingBean.java
    │           │   │   ├── ListableBeanFactory.java
    │           │   │   └── PropertyPlaceholderConfigurer.java
    │           │   ├── BeansException.java
    │           │   ├── PropertyValue.java
    │           │   └── PropertyValues.java 
    │           ├── context
    │           │   ├── annotation
    │           │   │   ├── ClassPathBeanDefinitionScanner.java 
    │           │   │   ├── ClassPathScanningCandidateComponentProvider.java 
    │           │   │   └── Scope.java 
    │           │   ├── event
    │           │   │   ├── AbstractApplicationEventMulticaster.java 
    │           │   │   ├── ApplicationContextEvent.java 
    │           │   │   ├── ApplicationEventMulticaster.java 
    │           │   │   ├── ContextClosedEvent.java 
    │           │   │   ├── ContextRefreshedEvent.java 
    │           │   │   └── SimpleApplicationEventMulticaster.java 
    │           │   ├── support
    │           │   │   ├── AbstractApplicationContext.java 
    │           │   │   ├── AbstractRefreshableApplicationContext.java 
    │           │   │   ├── AbstractXmlApplicationContext.java 
    │           │   │   ├── ApplicationContextAwareProcessor.java 
    │           │   │   └── ClassPathXmlApplicationContext.java 
    │           │   ├── ApplicationContext.java 
    │           │   ├── ApplicationContextAware.java 
    │           │   ├── ApplicationEvent.java 
    │           │   ├── ApplicationEventPublisher.java 
    │           │   ├── ApplicationListener.java 
    │           │   └── ConfigurableApplicationContext.java
    │           ├── core.io
    │           │   ├── ClassPathResource.java 
    │           │   ├── DefaultResourceLoader.java 
    │           │   ├── FileSystemResource.java 
    │           │   ├── Resource.java 
    │           │   ├── ResourceLoader.java
    │           │   └── UrlResource.java
    │           ├── stereotype
    │           │   └── Component.java
    │           └── utils
    │               ├── ClassUtils.java
    │               └── StringValueResolver.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── IUserService.java
                │   └── UserService.java
                └── ApiTest.java

工程源码 公众号「bugstack 虫洞栈」,回复:Spring 专栏,获取残缺源码

主动扫描注入占位符配置和对象的类关系,如图 15-2

  • 在整个类图中以围绕实现接口 InstantiationAwareBeanPostProcessor 的类 AutowiredAnnotationBeanPostProcessor 作为入口点,被 AbstractAutowireCapableBeanFactory 创立 Bean 对象过程中调用扫描整个类的属性配置中含有自定义注解 ValueAutowiredQualifier,的属性值。
  • 这里稍有变动的是对于属性值信息的获取,在注解配置的属性字段扫描到信息注入时,包含了占位符从配置文件获取信息也包含 Bean 对象,Bean 对象能够间接获取,但配置信息须要在 AbstractBeanFactory 中增加新的属性汇合 embeddedValueResolvers,由 PropertyPlaceholderConfigurer#postProcessBeanFactory 进行操作填充到属性汇合中。

2. 把读取到属性填充到容器

定义解析字符串接口

cn.bugstack.springframework.util.StringValueResolver

public interface StringValueResolver {String resolveStringValue(String strVal);

}
  • 接口 StringValueResolver 是一个解析字符串操作的接口

填充字符串

public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        try {
            // 加载属性文件
            DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
            Resource resource = resourceLoader.getResource(location);
            
            // ... 占位符替换属性值、设置属性值

            // 向容器中增加字符串解析器,供解析 @Value 注解应用
            StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(properties);
            beanFactory.addEmbeddedValueResolver(valueResolver);
            
        } catch (IOException e) {throw new BeansException("Could not load properties", e);
        }
    }

    private class PlaceholderResolvingStringValueResolver implements StringValueResolver {

        private final Properties properties;

        public PlaceholderResolvingStringValueResolver(Properties properties) {this.properties = properties;}

        @Override
        public String resolveStringValue(String strVal) {return PropertyPlaceholderConfigurer.this.resolvePlaceholder(strVal, properties);
        }

    }

}
  • 在解析属性配置的类 PropertyPlaceholderConfigurer 中,最次要的其实就是这行代码的操作 beanFactory.addEmbeddedValueResolver(valueResolver) 这是把属性值写入到了 AbstractBeanFactory 的 embeddedValueResolvers 中。
  • 这里阐明下,embeddedValueResolvers 是 AbstractBeanFactory 类新减少的汇合 List<StringValueResolver> embeddedValueResolvers String resolvers to apply e.g. to annotation attribute values

3. 自定义属性注入注解

自定义注解,Autowired、Qualifier、Value

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD})
public @interface Autowired {
}

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {String value() default "";

}  

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {

    /**
     * The actual value expression: e.g. "#{systemProperties.myProp}".
     */
    String value();}
  • 3 个注解在咱们日常应用 Spring 也是十分常见的,注入对象、注入属性,而 Qualifier 个别与 Autowired 配合应用。

4. 扫描自定义注解

cn.bugstack.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor

public class AutowiredAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor, BeanFactoryAware {

    private ConfigurableListableBeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }

    @Override
    public PropertyValues postProcessPropertyValues(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        // 1. 解决注解 @Value
        Class<?> clazz = bean.getClass();
        clazz = ClassUtils.isCglibProxyClass(clazz) ? clazz.getSuperclass() : clazz;

        Field[] declaredFields = clazz.getDeclaredFields();

        for (Field field : declaredFields) {Value valueAnnotation = field.getAnnotation(Value.class);
            if (null != valueAnnotation) {String value = valueAnnotation.value();
                value = beanFactory.resolveEmbeddedValue(value);
                BeanUtil.setFieldValue(bean, field.getName(), value);
            }
        }

        // 2. 解决注解 @Autowired
        for (Field field : declaredFields) {Autowired autowiredAnnotation = field.getAnnotation(Autowired.class);
            if (null != autowiredAnnotation) {Class<?> fieldType = field.getType();
                String dependentBeanName = null;
                Qualifier qualifierAnnotation = field.getAnnotation(Qualifier.class);
                Object dependentBean = null;
                if (null != qualifierAnnotation) {dependentBeanName = qualifierAnnotation.value();
                    dependentBean = beanFactory.getBean(dependentBeanName, fieldType);
                } else {dependentBean = beanFactory.getBean(fieldType);
                }
                BeanUtil.setFieldValue(bean, field.getName(), dependentBean);
            }
        }

        return pvs;
    }

}
  • AutowiredAnnotationBeanPostProcessor 是实现接口 InstantiationAwareBeanPostProcessor 的一个用于在 Bean 对象实例化实现后,设置属性操作前的解决属性信息的类和操作方法。只有实现了 BeanPostProcessor 接口才有机会在 Bean 的生命周期中解决初始化信息
  • 外围办法 postProcessPropertyValues,次要用于解决类含有 @Value、@Autowired 注解的属性,进行属性信息的提取和设置。
  • 这里须要留神一点因为咱们在 AbstractAutowireCapableBeanFactory 类中应用的是 CglibSubclassingInstantiationStrategy 进行类的创立,所以在 AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues 中须要判断是否为 CGlib 创建对象,否则是不能正确拿到类信息的。ClassUtils.isCglibProxyClass(clazz) ? clazz.getSuperclass() : clazz;

5. 在 Bean 的生命周期中调用属性注入

cn.bugstack.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy();

    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
        Object bean = null;
        try {
            // 判断是否返回代理 Bean 对象
            bean = resolveBeforeInstantiation(beanName, beanDefinition);
            if (null != bean) {return bean;}
            // 实例化 Bean
            bean = createBeanInstance(beanDefinition, beanName, args);
            // 在设置 Bean 属性之前,容许 BeanPostProcessor 批改属性值
            applyBeanPostProcessorsBeforeApplyingPropertyValues(beanName, bean, beanDefinition);
            // 给 Bean 填充属性
            applyPropertyValues(beanName, bean, beanDefinition);
            // 执行 Bean 的初始化办法和 BeanPostProcessor 的前置和后置解决办法
            bean = initializeBean(beanName, bean, beanDefinition);
        } catch (Exception e) {throw new BeansException("Instantiation of bean failed", e);
        }

        // 注册实现了 DisposableBean 接口的 Bean 对象
        registerDisposableBeanIfNecessary(beanName, bean, beanDefinition);

        // 判断 SCOPE_SINGLETON、SCOPE_PROTOTYPE
        if (beanDefinition.isSingleton()) {registerSingleton(beanName, bean);
        }
        return bean;
    }

    /**
     * 在设置 Bean 属性之前,容许 BeanPostProcessor 批改属性值
     *
     * @param beanName
     * @param bean
     * @param beanDefinition
     */
    protected void applyBeanPostProcessorsBeforeApplyingPropertyValues(String beanName, Object bean, BeanDefinition beanDefinition) {for (BeanPostProcessor beanPostProcessor : getBeanPostProcessors()) {if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor){PropertyValues pvs = ((InstantiationAwareBeanPostProcessor) beanPostProcessor).postProcessPropertyValues(beanDefinition.getPropertyValues(), bean, beanName);
                if (null != pvs) {for (PropertyValue propertyValue : pvs.getPropertyValues()) {beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
                    }
                }
            }
        }
    }  

    // ...
}
  • AbstractAutowireCapableBeanFactory#createBean 办法中有这一条新减少的办法调用,就是在 设置 Bean 属性之前,容许 BeanPostProcessor 批改属性值 的操作 applyBeanPostProcessorsBeforeApplyingPropertyValues
  • 那么这个 applyBeanPostProcessorsBeforeApplyingPropertyValues 办法中,首先就是获取曾经注入的 BeanPostProcessor 汇合并从中筛选出继承接口 InstantiationAwareBeanPostProcessor 的实现类。
  • 最初就是调用相应的 postProcessPropertyValues 办法以及循环设置属性值信息,beanDefinition.getPropertyValues().addPropertyValue(propertyValue);

五、测试

1. 当时筹备

配置 Dao

@Component
public class UserDao {private static Map<String, String> hashMap = new HashMap<>();

    static {hashMap.put("10001", "小傅哥,北京,亦庄");
        hashMap.put("10002", "八杯水,上海,尖沙咀");
        hashMap.put("10003", "阿毛,香港,铜锣湾");
    }

    public String queryUserName(String uId) {return hashMap.get(uId);
    }

}
  • 给类配置上一个主动扫描注册 Bean 对象的注解 @Component,接下来会把这个类注入到 UserService 中。

注解注入到 UserService

@Component("userService")
public class UserService implements IUserService {@Value("${token}")
    private String token;

    @Autowired
    private UserDao userDao;

    public String queryUserInfo() {
        try {Thread.sleep(new Random(1).nextInt(100));
        } catch (InterruptedException e) {e.printStackTrace();
        }
        return userDao.queryUserName("10001") + "," + token;
    }    

    // ...
}
  • 这里包含了两种类型的注入,一个是占位符注入属性信息 @Value("${token}"),另外一个是注入对象信息 @Autowired

2. 属性配置文件

token.properties

token=RejDlI78hu223Opo983Ds

spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context">

    <bean class="cn.bugstack.springframework.beans.factory.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:token.properties"/>
    </bean>

    <context:component-scan base-package="cn.bugstack.springframework.test.bean"/>

</beans>
  • 在 spring.xml 中配置了扫描属性信息和主动扫描包门路范畴。

3. 单元测试

@Test
public void test_scan() {ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
    IUserService userService = applicationContext.getBean("userService", IUserService.class);
    System.out.println("测试后果:" + userService.queryUserInfo());
}
  • 单元测试时候就能够残缺的测试一个类注入到 Spring 容器,同时这个属性信息也能够被主动扫描填充上。

测试后果

测试后果:小傅哥,北京,亦庄,RejDlI78hu223Opo983Ds

Process finished with exit code 0

  • 从测试后果能够看到当初咱们的应用形式曾经通过了,有主动扫描类,有注解注入属性。这与应用 Spring 框架越来越像了。

六、总结

  • 从整个注解信息扫描注入的实现内容来看,咱们始终是围绕着在 Bean 的生命周期中进行解决,就像 BeanPostProcessor 用于批改新实例化 Bean 对象的扩大点,提供的接口办法能够用于解决 Bean 对象实例化前后进行解决操作。而有时候须要做一些差异化的管制,所以还须要继承 BeanPostProcessor 接口,定义新的接口 InstantiationAwareBeanPostProcessor 这样就能够辨别出不同扩大点的操作了。
  • 像是接口用 instanceof 判断,注解用 Field.getAnnotation(Value.class); 获取,都是相当于在类上做的一些标识性信息,便于能够用一些办法找到这些性能点,以便进行解决。所以在咱们日常开发设计的组件中,也能够使用上这些特点。
  • 当你思考把你的实现融入到一个曾经细分好的 Bean 生命周期中,你会发现它的设计是如此的好,能够让你在任何初始化的工夫点上,任何面上,都能做你须要的扩大或者扭转,这也是咱们做程序设计时谋求的灵活性。

七、系列举荐

  • 调研字节码插桩技术,用于系统监控设计和实现
  • 工作两三年了,整不明确架构图都画啥?
  • Thread.start(),它是怎么让线程启动的呢?-%E5%AE%83%E6%98%AF%E6%80%8E%E4%B9%88%E8%AE%A9%E7%BA%BF%E7%A8%8B%E5%90%AF%E5%8A%A8%E7%9A%84%E5%91%A2.html)
  • 基于 jdbc 实现一个 Demo 版的 Mybatis
  • 一个 Bug,让我发现了 Java 界的.AJ(锥)!.html)

正文完
 0