mini-Spring 代理篇-AOP:Step 14:使用注解实现配置属性和 Bean 对象的自动注入

引言

在Spring框架中,依赖注入(Dependency Injection)是核心机制之一,用于管理Bean对象的创建和依赖关系。传统方式依赖XML配置文件手动指定属性值和Bean引用,这种方法在复杂项目中容易导致配置繁琐和维护困难。为简化开发流程,提高代码的可读性和灵活性,Spring引入注解-based注入机制,如@Value用于注入配置值,@Autowired用于注入Bean对象。

本章聚焦于实现注解注入配置值和Bean对象的功能。实现思路源于Bean生命周期的扩展点:Spring的BeanPostProcessor接口允许在Bean实例化后、属性填充前修改属性值。通过继承InstantiationAwareBeanPostProcessor接口,并在postProcessPropertyValues方法中扫描注解,可以在属性填充阶段动态注入值。这种方法避免了XML配置的静态性,利用反射和注解元数据实现自动化注入。

具体步骤包括:

(1)在BeanFactory中注册属性解析器处理占位符;

(2)定义自定义注解;

(3)创建注解处理器扫描并注入属性;

(4)在Bean创建流程中调用处理器,确保注入发生在属性填充前。该设计确保兼容现有Bean生命周期,同时提供可扩展性。

目标

实现基于注解的自动注入功能,包括:

  • 使用@Value注解注入配置值(如从属性文件读取的占位符)。
  • 使用@Autowired注解注入Bean对象,支持@Qualifier指定Bean名称。 此功能构建于IOC容器自动扫描@Component注解的基础上,取代XML中的<property>标签。</property>

设计方案

注入操作集成到Bean生命周期的属性填充阶段(applyPropertyValues前)。核心组件包括:

  • 属性解析器:使用BeanFactoryPostProcessor(如PropertyPlaceholderConfigurer)加载属性文件,并注册StringValueResolver到BeanFactory,用于解析@Value中的占位符。
  • 注解处理器:实现InstantiationAwareBeanPostProcessor接口,在postProcessPropertyValues方法中扫描字段注解,注入配置值或Bean实例。
  • 注入逻辑
    • 配置值:从BeanFactory解析占位符(如”${token}”),设置到PropertyValues。
    • Bean对象:通过BeanFactory.getBean获取实例,支持类型或名称匹配。
  • 该方案利用BeanPostProcessor的扩展性,确保注入不干扰核心创建流程。

结构:

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
│ │ │ ├── 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

2.自动扫描注入占位符配置和对象的类关系:

图 15-2

实现:

1.定义解析字符串接口:

定义StringValueResolver接口,用于解析占位符字符串。

/**
 * 字符串值解析器接口。
 * 定义了解析字符串的方法,通常用于解析占位符或表达式,
 * 将输入字符串转换为解析后的实际值。
 */
public interface StringValueResolver {
    String resolveStringValue(String strVal);
}

2.填充属性值到容器:

2.1 新增集合embeddedValueResolvers :

在 AbstractBeanFactory 中新增了一个集合:

/*
    embeddedValueResolvers 集合存储 StringValueResolver 实例,
    用于解析占位符字符串(如 ${token})以注入配置值或动态值到Bean的属性或注解中。
     */
    private final List<StringValueResolver> embeddedValueResolvers = new ArrayList<>();

在ConfigurableBeanFactory定义方法:

/**
     * 添加一个字符串解析器,用于解析嵌入的值,例如注解属性。
     */
    void addEmbeddedValueResolver(StringValueResolver valueResolver);

    /**
     * 解析给定的嵌入值,例如注解属性。
     */
    String resolveEmbeddedValue(String value);

并在AbstractBeanFactory 中实现:

@Override
    public void addEmbeddedValueResolver(StringValueResolver valueResolver) {
        this.embeddedValueResolvers.add(valueResolver);
    }

    @Override
    public String resolveEmbeddedValue(String value) {
        String result = value;
        for (StringValueResolver resolver : this.embeddedValueResolvers) {
            result = resolver.resolveStringValue(result);
        }
        return result;
    }

2.2 拓展PropertyPlaceholderConfigurer:

优化逻辑并新增valueResolver 相关功能:

/**
 * 该类实现了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 {
            // 1. 创建资源加载器,加载指定位置的属性文件
            DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
            Resource resource = resourceLoader.getResource(location);

            // 2. 读取属性文件内容到 Properties 对象中
            Properties properties = new Properties();
            properties.load(resource.getInputStream());

            // 3. 遍历容器中所有 BeanDefinition
            String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
            for (String beanName : beanDefinitionNames) {
                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);

                // 4. 获取该 BeanDefinition 的所有属性值
                PropertyValues propertyValues = beanDefinition.getPropertyValues();
                for (PropertyValue propertyValue : propertyValues.getPropertyValues()) {
                    Object value = propertyValue.getValue();

                    // 5. 只处理字符串类型的属性值
                    if (!(value instanceof String)) continue;

                    // 6. 替换属性值中的占位符(如 ${xxx})
                    value = resolvePlaceholder((String) value, properties);

                    // 7. 将替换后的值重新写回到属性列表
                    propertyValues.addPropertyValue(new PropertyValue(propertyValue.getName(), value));
                }
            }

            // 8. 向 BeanFactory 注册一个字符串解析器
            //    这个解析器会在解析 @Value 注解时生效,用于处理占位符
            StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(properties);
            beanFactory.addEmbeddedValueResolver(valueResolver);

        } catch (IOException e) {
            // 9. 属性文件加载失败时,抛出 BeansException
            throw new BeansException("Could not load properties", e);
        }
    }


    /**
     * 解析字符串中的占位符并替换为对应的属性值。
     * 占位符格式为 `${key}`,从给定的 Properties 中获取 key 对应的值进行替换。
     * 如果字符串中不存在占位符,则返回原字符串。
     */
    private String resolvePlaceholder(String value, Properties properties) {
        String strVal = value;
        StringBuilder buffer = new StringBuilder(strVal);
        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);
            buffer.replace(startIdx, stopIdx + 1, propVal);
        }
        return buffer.toString();
    }


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

    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);
        }

    }

}

3.自定义属性注入注解

/**
 * 值注入注解。
 * 用于将外部配置或表达式的值注入到字段、方法参数等位置。
 */
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {

    /**
     * 要注入的实际值或表达式,例如 "#{systemProperties.myProp}"。
     */
    String value();
}
/**
 * 自动注入注解。
 * <p>用于标记构造器、字段或方法,表示依赖由 Spring 容器自动装配。
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD})
public @interface Autowired {
}
/**
 * 限定符注解。
 * <p>与 @Autowired 配合使用,用于按名称精确指定注入的 Bean。
 */
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {

    /**
     * 指定注入的 Bean 名称。
     */
    String value() default "";
}

4. 扫描注解

实现AutowiredAnnotationBeanPostProcessor,处理注解注入。

此处又提到了AutowiredAnnotationBeanPostProcessor,那我们就来加深下记忆:

特性BeanPostProcessorInstantiationAwareBeanPostProcessorBeanFactoryPostProcessor
作用对象针对 Bean 实例 进行处理针对 Bean 实例化过程 进行干预针对 BeanFactory(容器) 及 BeanDefinition 进行处理
作用阶段Bean 实例化 之后(构造方法执行后)Bean 实例化 之前 + 实例化后、属性填充前Bean 实例化 之前(容器启动初期,BeanDefinition 加载后)
核心方法– postProcessBeforeInitialization()
– postProcessAfterInitialization()
– postProcessBeforeInstantiation()
– postProcessAfterInstantiation()
– postProcessProperties()
– postProcessBeanFactory()
典型用途实例初始化前后增强(如 AOP 代理、添加属性)控制实例化过程(如返回代理替代原始对象)、属性注入前处理(如解析 @Autowired修改 BeanDefinition(如动态注册 Bean、修改属性值)、自定义 BeanFactory 配置
执行时机在 InitializingBean.afterPropertiesSet() 或自定义初始化方法前后在 Bean 构造方法调用前、后,以及属性填充前在所有 BeanDefinition 加载完成,但未开始实例化任何 Bean 时
常见实现类AnnotationAwareAspectJAutoProxyCreator(AOP 代理)AutowiredAnnotationBeanPostProcessor(处理 @AutowiredPropertyPlaceholderConfigurer(处理 @Value 占位符)、ConfigurationClassPostProcessor(处理 @Configuration
是否修改 Bean 定义不修改,仅处理已实例化的 Bean不直接修改 BeanDefinition,仅干预实例化过程直接修改 BeanDefinition(如属性、作用域等)

4.1 前置修改:

又到了这个教程最爱的莫名其妙回马枪的时间了:

BeanFactory添加:

<T> T getBean(Class<T> requiredType) throws BeansException;

所以DefaultListableBeanFactory实现:

@Override
public <T> T getBean(Class<T> requiredType) throws BeansException {
List<String> beanNames = new ArrayList<>();
for (Map.Entry<String, BeanDefinition> entry : beanDefinitionMap.entrySet()) {
Class beanClass = entry.getValue().getBeanClass();
if (requiredType.isAssignableFrom(beanClass)) {
beanNames.add(entry.getKey());
}
}
if (1 == beanNames.size()) {
return getBean(beanNames.get(0), requiredType);
}

throw new BeansException(requiredType + "expected single bean but found " + beanNames.size() + ": " + beanNames);
}

同样的AbstractApplicationContext实现:

@Override
public <T> T getBean(Class<T> requiredType) throws BeansException {
return getBeanFactory().getBean(requiredType);
}

InstantiationAwareBeanPostProcessor 添加:

/**
     * 在工厂将属性值应用到给定 Bean 之前,对这些属性值进行后置处理。
     * 例如,可以在这里检查所有依赖是否已满足,
     * 也可以基于 Bean 属性的 setter 方法上的 “@Required” 注解进行依赖校验。
     *
     * 在 Bean 实例化完成后、执行属性填充操作之前调用此方法。
     *
     */
    PropertyValues postProcessPropertyValues(PropertyValues pvs, Object bean, String beanName) throws BeansException;

而DefaultAdvisorAutoProxyCreator实现了InstantiationAwareBeanPostProcessor;

添加实现:
@Override
public PropertyValues postProcessPropertyValues(PropertyValues pvs, Object bean, String beanName) throws BeansException {
return pvs;
}

5.注册注解处理器

由于AutowiredAnnotationBeanPostProcessor并没有标注@Component,所以是无法在类扫描时注入到beanFactory中的,此处需要我们手动进行注册。
是 Spring 框架的内部组件,旨在为所有启用注解扫描的项目提供支持,所以需要在spring内部注册,而不是像上一篇的PropertyPlaceholderConfigurer手动配置为Bean。

在ClassPathBeanDefinitionScanner中手动注册AutowiredAnnotationBeanPostProcessor:

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
    // ...
    public void doScan(String... basePackages) {
        // 扫描并注册BeanDefinition
        // ...
        registry.registerBeanDefinition("internalAutowiredAnnotationProcessor", new BeanDefinition(AutowiredAnnotationBeanPostProcessor.class));
    }
}

虽然是被手动配置为bean的,但是触发条件:注册过程由 或 @ComponentScan 触发,发生在 XmlBeanDefinitionReader的 doLoadBeanDefinitions方法中,此时就会注册一个ClassPathBeanDefinitionScanner,然后将AutowiredAnnotationBeanPostProcessor注册到Bean注册容器中。


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

AbstractAutowireCapableBeanFactory

createBean中:

******
        try {
            // 创建实例
            bean = createBeanInstance(beanDefinition, beanName, args);
        } catch (Exception e) {
            throw new BeansException("Failed to create bean instance for: " + beanName, e);
        }

        try {
            // 在设置 Bean 属性之前,允许 AutowiredAnnotationBeanPostProcessor 修改属性值(@Autowired、@Value)
            applyBeanPostProcessorsBeforeApplyingPropertyValues(beanName, bean, beanDefinition);
        } catch (Exception e) {
            throw new BeansException("Failed to process @Autowired/@Value annotations for bean: " + beanName, e);
        }

        try {
            // 注入属性
            applyPropertyValues(bean, beanName, beanDefinition);
        } catch (Exception e) {
            throw new BeansException("Failed to apply property values for bean: " + beanName, e);
        }
******
/**
     * 在为 Bean 填充属性(依赖注入)之前,允许 BeanPostProcessor 对属性值进行修改。
     *
     * 这里主要是为了支持类似 @Autowired、@Value、@Resource 等注解驱动的依赖注入,
     * 因为它们依赖于 InstantiationAwareBeanPostProcessor 在属性设置前进行解析与赋值。
     *
     * @param beanName        当前 Bean 的名称
     * @param bean            已经实例化好的 Bean 对象(构造方法已调用,但属性还未注入)
     * @param beanDefinition  Bean 对应的定义信息(包含属性、构造参数等)
     */
    protected void applyBeanPostProcessorsBeforeApplyingPropertyValues(String beanName, Object bean, BeanDefinition beanDefinition) {
        // 遍历当前容器中注册的所有 BeanPostProcessor
        for (BeanPostProcessor beanPostProcessor : getBeanPostProcessors()) {
            // 只有 InstantiationAwareBeanPostProcessor 才能在属性设置前做处理
            if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor) {
                // 调用其 postProcessPropertyValues 方法,让它有机会修改 Bean 的属性值
                // 例如 AutowiredAnnotationBeanPostProcessor 会在这里解析 @Autowired 并生成对应的依赖注入 PropertyValue
                PropertyValues pvs = ((InstantiationAwareBeanPostProcessor) beanPostProcessor)
                        .postProcessPropertyValues(beanDefinition.getPropertyValues(), bean, beanName);

                // 如果处理器返回了新的属性值集合,则合并到 BeanDefinition 的属性列表中
                if (null != pvs) {
                    for (PropertyValue propertyValue : pvs.getPropertyValues()) {
                        beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
                    }
                }
            }
        }
    }

测试:

1.测试类:

@Component
public class UserDao {

    private static Map<String, String> hashMap = new HashMap<>();

    static {
        hashMap.put("10001", "CMD137,北京,亦庄");
        hashMap.put("10002", "CMD138,上海,尖沙咀");
        hashMap.put("10003", "CMD139,天津,东丽区");
    }

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

}

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

@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;
}

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

public String getToken() {
return token;
}

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

2.配置文件:

token.properties

token=123_TEST_TOKEN_321

spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <bean class="com.miniSpring.beans.factory.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:token.properties"/>
    </bean>

    <component-scan base-package="com.miniSpring.test.bean"/>
</beans>

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());
}
测试结果:CMD137,北京,亦庄,123_TEST_TOKEN_321

Process finished with exit code 0

暂无评论

发送评论 编辑评论


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