mini-Spring 代理篇-AOP:Step 13:通过注解配置和包自动扫描的方式完成Bean对象的注册

前言:为何引入注解 + 扫描机制

在传统的 Spring 应用中,开发者需要在 XML 中显式配置每个 Bean,这种方式一方面繁琐、难以维护;另一方面如果 Bean 数量多、依赖复杂,还容易出错。现代应用更倾向于“约定优于配置”,实现自动化注册:

  • 开发者只需在类上增加注解;
  • 框架通过扫描指定包路径,自动识别并注册所有相关组件;
  • 配置化属性(如 ${...})在 BeanDefinition 加载完成后注入,支持属性占位符灵活管理。

本章手撸 Spring,将采用自定义注解与包扫描方式替代 XML 的显式注册,并结合 BeanFactoryPostProcessor 实现占位符配置注入。

一、功能目标

  1. 注解标记 Bean:通过自定义注解(如 @Component@Scope)标识候选 Bean;
  2. 包路径扫描:解析 XML 中 <context:component-scan> 指定扫描路径;
  3. 类加载与注解识别:扫描类文件,识别注解,读取 bean 名称和作用域;
  4. BeanDefinition 注册:根据扫描结果组装 BeanDefinition 并注册到容器;
  5. 属性占位符注入:实现 ${…} 占位符解析,通过读取配置文件填充属性。

通过上述功能,将自动完成 Bean 的定义、注册与属性注入,减少配置冗余,提升开发效率和系统灵活性。

二、设计方案概览

核心组件如下:

  • 工程结构:增加 context.annotation 包用于注解与扫描机制;
  • 注解定义@Component 标识 Bean,@Scope 定义作用域;
  • 扫描工具
    • ClassPathScanningCandidateComponentProvider 扫描包中的注解类;
    • ClassPathBeanDefinitionScanner 解析注解、作用域和 Bean 名称;
  • XML 解析增强:在 XmlBeanDefinitionReader#doLoadBeanDefinitions 中加入扫描解析 <context:component-scan>
  • 占位符注入:通过实现 BeanFactoryPostProcessor,在 BeanDefinition 加载后、实例化前处理占位符。

整体流程如下:

  1. 读取 XML 配置;
  2. 在XmlBeanDefinitionReader中解析,遇到 <context:component-scan>,触发扫描;
  3. 扫描注解类并生成 BeanDefinition;
  4. 将 BeanDefinition 注入 BeanFactory;
  5. 应用 PropertyPlaceholderConfigurer 解析 ${…} 并替换为配置值;
  6. 按常规流程实例化 Bean。

三、架构:

1. 工程结构

small-spring-step-13
└── 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
    │           │   │   ├── 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
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── IUserService.java
                │   └── UserService.java
                └── ApiTest.java

2.在Bean的生命周期中自动加载包扫描注册Bean对象和设置占位符属性的类关系

图 14-2

四、实现详解:

1.注解定义

前置知识:JAVA-注解 Annotation – CMD137’s Blog

/**
 * 用于配置作用域的自定义注解,方便通过配置Bean对象注解的时候,拿到Bean对象的作用域。不过一般都使用默认的 singleton
 */
@Target({ElementType.TYPE, ElementType.METHOD}) // 可作用于类和方法
@Retention(RetentionPolicy.RUNTIME)             // 运行时可通过反射读取
@Documented                                     // 生成 Javadoc 时包含该注解信息
public @interface Scope {
    /**
     * 作用域值,默认为 singleton。
     * @return 作用域名称
     */
    String value() default "singleton";
}

/**
 * 组件注解,用于标识一个类为Spring容器管理的Bean
 * 被此注解标记的类会被自动扫描并注册到Spring容器中
 */
@Target(ElementType.TYPE)          // 仅可作用于类、接口(包括注解类型)、枚举
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射读取
@Documented                         // 生成 Javadoc 时包含该注解信息
public @interface Component {
    /**
     * Bean 名称,可选。
     * @return Bean 名称
     */
    String value() default "";
}

2.处理对象扫描装配:

该类的核心作用是在指定的包路径下扫描带有特定注解的类,并将这些类转换为组件定义(BeanDefinition),为后续的对象创建和管理提供元数据。

/**
* 在基于注解的 Bean 注册机制中,需要有一个步骤能从指定包路径中发现符合条件的类,
* 并将它们作为候选组件(Candidate Component)交给 BeanDefinition 注册流程处理。
* 该类就是完成扫描 + 转换这一功能的核心工具类。
*/

public class ClassPathScanningCandidateComponentProvider {

/**
* 扫描指定基础包路径,查找被 {@link Component} 注解标记的类,
* 并将其封装为 {@link BeanDefinition} 对象集合返回。
*
* @param basePackage 基础包路径(如 "com.example.service"
* @return 候选 BeanDefinition 集合
*/
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
// 用 LinkedHashSet 保证扫描结果有序且不重复
Set<BeanDefinition> candidates = new LinkedHashSet<>();

// 调用工具方法扫描出被 @Component 标记的类
Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(basePackage, Component.class);

// 将扫描到的类转换为 BeanDefinition 并加入集合
for (Class<?> clazz : classes) {
candidates.add(new BeanDefinition(clazz));
}

return candidates;
}
}

ClassPathBeanDefinitionScanner 是继承自 ClassPathScanningCandidateComponentProvider 的具体扫描包处理的类,在 doScan 中除了获取到扫描的类信息以后,还需要获取 Bean 的作用域和类名,

/**
 * 该类的核心作用是在指定包路径下扫描符合条件的组件(继承父类的扫描能力),
 * 并将这些组件的定义(BeanDefinition)注册到 Bean 定义注册表(BeanDefinitionRegistry)中,
 * 是连接 “组件扫描” 与 “容器注册” 的关键桥梁。
 */
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {

    // BeanDefinition 注册器,用于将扫描到的 BeanDefinition 注册到容器
    private BeanDefinitionRegistry registry;

    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
        this.registry = registry;
    }

    /**
     * 扫描指定的基础包路径,并注册符合条件的 BeanDefinition
     */
    public void doScan(String... basePackages) {
        for (String basePackage : basePackages) {
            // 扫描该包下所有被 @Component 标注的类,转换成 BeanDefinition 集合
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);

            for (BeanDefinition beanDefinition : candidates) {
                // 解析 Bean 的作用域(singleton、prototype 等)
                String beanScope = resolveBeanScope(beanDefinition);
                if (StrUtil.isNotEmpty(beanScope)) {
                    beanDefinition.setScope(beanScope);
                }

                // 注册 BeanDefinition 到容器
                // determineBeanName() 用于解析 Bean 名称(@Component value 或类名首字母小写)
                registry.registerBeanDefinition(determineBeanName(beanDefinition), beanDefinition);
            }
        }
    }

    /**
     * 根据 @Scope 注解解析 Bean 的作用域
     */
    private String resolveBeanScope(BeanDefinition beanDefinition) {
        Class<?> beanClass = beanDefinition.getBeanClass();
        Scope scope = beanClass.getAnnotation(Scope.class);
        if (null != scope) return scope.value(); // 如果有 @Scope 注解,返回其值
        return StrUtil.EMPTY; // 默认返回空字符串,表示使用默认作用域(singleton)
    }

    /**
     * 根据 @Component 注解解析 Bean 的名称
     * 如果 @Component value 为空,则使用类名首字母小写
     */
    private String determineBeanName(BeanDefinition beanDefinition) {
        Class<?> beanClass = beanDefinition.getBeanClass();
        Component component = beanClass.getAnnotation(Component.class);
        String value = component.value();
        if (StrUtil.isEmpty(value)) {
            // 例如 UserService -> userService
            value = StrUtil.lowerFirst(beanClass.getSimpleName());
        }
        return value;
    }
}

此处简单提一嘴同名Bean的问题:

真实 Spring 的处理逻辑
在 Spring 框架中,DefaultListableBeanFactory.registerBeanDefinition 会先检查容器中是否已存在同名 BeanDefinition。如果存在,默认允许覆盖,并会在日志中输出覆盖信息,提示开发者哪个 BeanDefinition 被替换。如果开发者将 allowBeanDefinitionOverriding 设置为 false,则遇到同名 Bean 会抛出 BeanDefinitionOverrideException,从而禁止重复注册。这种机制既保证了启动灵活性,又提供了安全检查与调试提示。

mini-spring 的处理逻辑
在 mini-spring 中,ClassPathBeanDefinitionScanner 扫描到的同名 Bean 在注册时直接调用 BeanDefinitionRegistry.registerBeanDefinition 存入 Map。由于默认没有做重复检测,后注册的 BeanDefinition 会直接覆盖之前注册的同名 Bean,容器中只保留最后一个注册的 Bean。

3.处理占位符配置 PropertyPlaceholderConfigurer:

功能

  • 用于解析 Bean 定义中属性的占位符(placeholder),例如 ${property.name}
  • 将占位符替换为真实的值,通常来自外部配置文件(.properties)或者环境变量。

作用

避免硬编码:例如数据库 URL、用户名、密码、API Key 等可以统一在配置文件管理。
实现属性外部化:配置与代码分离,提高灵活性。
支持统一管理:多个 Bean 可以共享同一个配置属性。

PropertyPlaceholderConfigurer 本质上是一个 BeanFactoryPostProcessor,它在 Bean 实例化之前工作。流程如下:

  1. 加载配置文件
    • 通过 location 属性读取 .properties 文件,加载到 Properties 对象中。
  2. 遍历 BeanDefinition
    • 遍历容器中所有已注册的 BeanDefinition(此时 Bean 还未实例化)。
    • 查找属性值中是否包含占位符 ${...}
  3. 占位符替换
    • 根据 key 从 Properties 中获取对应的值。
    • 将 BeanDefinition 的属性值替换成真实值。
  4. Bean 实例化时使用
    • 当容器真正实例化 Bean 时,属性已经是最终替换后的值,不需要再次解析。
/**
 * 该类实现了Spring的BeanFactoryPostProcessor接口,
 * 用于处理Bean定义中的属性占位符,将${...}形式的占位符替换为属性文件中的实际值
 */
public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {

    /**
     * 默认的占位符前缀: {@value}
     */
    public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";

    /**
     * 默认的占位符后缀: {@value}
     */
    public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";

    // 属性文件的位置
    private String location;

    /**
     * 实现BeanFactoryPostProcessor接口的方法,在BeanFactory加载完所有Bean定义后执行
     * 用于替换Bean定义中的属性占位符
     * @param beanFactory 可配置的Bean工厂,用于获取和修改Bean定义
     * @throws BeansException 如果处理过程中发生错误
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 加载属性文件并替换占位符
        try {
            // 创建默认的资源加载器,用于加载属性文件
            DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
            // 根据配置的位置加载资源
            Resource resource = resourceLoader.getResource(location);
            // 创建Properties对象并加载资源中的属性
            Properties properties = new Properties();
            properties.load(resource.getInputStream());

            // 获取所有Bean定义的名称
            String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
            // 遍历每个Bean定义
            for (String beanName : beanDefinitionNames) {
                // 获取当前Bean的定义信息
                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);

                // 获取Bean定义中的所有属性值
                PropertyValues propertyValues = beanDefinition.getPropertyValues();
                // 遍历每个属性值
                for (PropertyValue propertyValue : propertyValues.getPropertyValues()) {
                    // 获取属性值对象
                    Object value = propertyValue.getValue();
                    // 如果属性值不是字符串类型,则跳过
                    if (!(value instanceof String)) continue;
                    String strVal = (String) value;

                    // 查找占位符前缀的位置
                    int startIdx = strVal.indexOf(DEFAULT_PLACEHOLDER_PREFIX);
                    // 查找占位符后缀的位置
                    int stopIdx = strVal.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);

                    // 如果找到了有效的占位符(前缀在后缀之前)
                    if (startIdx != -1 && stopIdx != -1 && startIdx < stopIdx) {
                        // 提取占位符中的属性键(去掉前缀和后缀)
                        String propKey = strVal.substring(startIdx + 2, stopIdx);
                        // 从属性文件中获取对应的属性值
                        String propVal = properties.getProperty(propKey);
                        // 替换占位符为实际属性值
                        String newValue = strVal.replace(DEFAULT_PLACEHOLDER_PREFIX + propKey + DEFAULT_PLACEHOLDER_SUFFIX, propVal);
                        // 更新Bean定义中的属性值
                        propertyValues.addPropertyValue(new PropertyValue(propertyValue.getName(), newValue));
                    }
                }
            }
        } catch (IOException e) {
            // 如果加载属性文件失败,抛出异常
            throw new BeansException("Could not load properties", e);
        }
    }

    /**
     * 设置属性文件的位置
     * @param location 属性文件的路径
     */
    public void setLocation(String location) {
        this.location = location;
    }

}

4.解析xml(XmlBeanDefinitionReader)中调用扫描:

此处主要修改了doLoadBeanDefinitions,换了另一个包(dom4j)来解析xml。

protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException, DocumentException {
    // 1. 创建 SAXReader 对象,使用 dom4j 解析 XML
    SAXReader reader = new SAXReader();
    // 2. 读取输入流,生成 Document 对象(DOM 树)
    Document document = reader.read(inputStream);
    // 3. 获取根元素 <beans>
    Element root = document.getRootElement();

    // ---------------------------
    // 4. 解析 <context:component-scan> 标签
    //    目的是扫描指定包下的类并生成 BeanDefinition
    // ---------------------------
    Element componentScan = root.element("component-scan");
    if (componentScan != null) {
        // 5. 获取 base-package 属性值,支持逗号分隔多个包
        String scanPath = componentScan.attributeValue("base-package");
        // 6. 如果 base-package 为空,抛出异常
        if (StrUtil.isEmpty(scanPath)) {
            throw new BeansException("The value of base-package attribute can not be empty or null");
        }
        // 7. 调用 scanPackage 方法扫描包
        scanPackage(scanPath);
    }

    // ---------------------------
    // 8. 解析 <bean> 标签
    // ---------------------------
    List<Element> beanList = root.elements("bean");
    for (Element bean : beanList) {
        // 9. 获取 bean 标签的基本属性:id、name、class、init-method、destroy-method、scope
        String id = bean.attributeValue("id");
        String name = bean.attributeValue("name");
        String className = bean.attributeValue("class");
        String initMethod = bean.attributeValue("init-method");
        String destroyMethodName = bean.attributeValue("destroy-method");
        String beanScope = bean.attributeValue("scope");

        // 10. 根据 class 属性获取 Class 对象
        Class<?> clazz = Class.forName(className);
        // 11. 确定 Bean 名称,优先使用 id,其次 name,最后类名首字母小写
        String beanName = StrUtil.isNotEmpty(id) ? id : name;
        if (StrUtil.isEmpty(beanName)) {
            beanName = StrUtil.lowerFirst(clazz.getSimpleName());
        }

        // 12. 创建 BeanDefinition 对象,用于封装 bean 信息
        BeanDefinition beanDefinition = new BeanDefinition(clazz);
        beanDefinition.setInitMethodName(initMethod);
        beanDefinition.setDestroyMethodName(destroyMethodName);
        // 13. 如果 scope 不为空,设置到 BeanDefinition
        if (StrUtil.isNotEmpty(beanScope)) {
            beanDefinition.setScope(beanScope);
        }

        // ---------------------------
        // 14. 解析 <property> 子标签
        //    用于读取 Bean 属性并封装成 PropertyValue 对象
        // ---------------------------
        List<Element> propertyList = bean.elements("property");
        for (Element property : propertyList) {
            // 15. 获取属性名、属性值、引用
            String attrName = property.attributeValue("name");
            String attrValue = property.attributeValue("value");
            String attrRef = property.attributeValue("ref");
            // 16. 判断是引用其他 Bean 还是普通值
            Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue;
            // 17. 封装属性名和值到 PropertyValue
            PropertyValue propertyValue = new PropertyValue(attrName, value);
            // 18. 添加到 BeanDefinition 的属性集合中
            beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
        }

        // 19. 检查容器中是否已有同名 Bean,防止重复注册
        if (getRegistry().containsBeanDefinition(beanName)) {
            throw new BeansException("Duplicate beanName[" + beanName + "] is not allowed");
        }

        // 20. 注册 BeanDefinition 到注册表中
        getRegistry().registerBeanDefinition(beanName, beanDefinition);
    }
}

/**
 * 扫描指定的包路径,生成 BeanDefinition 并注册到容器
 * @param scanPath 逗号分隔的基础包路径
 */
private void scanPackage(String scanPath) {
    // 1. 分割多个包名
    String[] basePackages = StrUtil.splitToArray(scanPath, ',');
    // 2. 创建扫描器,传入 BeanDefinition 注册器
    ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(getRegistry());
    // 3. 执行扫描并注册 BeanDefinition
    scanner.doScan(basePackages);
}

测试:

1.测试类:

@Component("userService")
public class UserService implements IUserService {

    private String token;

    public String queryUserInfo() {
        try {
            Thread.sleep(new Random(1).nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "CMD137,100001,深圳";
    }

    public String register(String userName) {
        try {
            Thread.sleep(new Random(1).nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "注册用户:" + userName + " success!";
    }

    @Override
    public String toString() {
        return "UserService#token = { " + token + " }";
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

2.属性配置文件 classpath:token.properties:

token=123_TEST_TOKEN_321

3.xml 配置文件:

3.1 spring-property.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean class="com.miniSpring.beans.factory.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:token.properties"/>
    </bean>

    <bean id="userService" class="com.miniSpring.test.bean.UserService">
        <property name="token" value="${token}"/>
    </bean>

</beans>

3.2 spring-scan.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <component-scan base-package="com.miniSpring.test.bean"/>
</beans>

4.单元测试:

4.1 测试包扫描:

@Test
public void test_scan() {
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-scan.xml");
    IUserService userService = applicationContext.getBean("userService", IUserService.class);
    System.out.println("测试结果:" + userService.queryUserInfo());
}
测试结果:CMD137,100001,深圳

Process finished with exit code 0

4.2 测试占位符配置:

@Test
public void test_property() {
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-property.xml");
    IUserService userService = applicationContext.getBean("userService", IUserService.class);
    System.out.println("测试结果:" + userService);
}
测试结果:UserService#token = { 123_TEST_TOKEN_321 }

Process finished with exit code 0

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇