【源码分析】Spring Boot中Relaxed Binding机制的不同实现

简介: Relaxed Binding 是 Spring Boot 中一个有趣的机制,它可以让开发人员更灵活地进行配置。但偶然的机会,我发现这个机制在 Spring Boot 1.5 与 Spring Boot 2.0 版本中的实现是不一样的。

Relaxed Binding 是 Spring Boot 中一个有趣的机制,它可以让开发人员更灵活地进行配置。但偶然的机会,我发现这个机制在 Spring Boot 1.5 与 Spring Boot 2.0 版本中的实现是不一样的。

Relaxed Binding机制

关于Relaxed Binding机制在Spring Boot的官方文档中有描述,链接如下:
24.7 Type-safe Configuration Properties
24.7.2 Relaxed Binding
这个Relaxed Binding机制的主要作用是,当配置文件(properties或yml)中的配置项值与带@ConfigurationProperties注解的配置类属性进行绑定时,可以进行相对宽松的绑定。
本文并不是针对@ConfigurationProperties与类型安全的属性配置进行解释,相关的说明请参考上面的链接。

Relax Binding示例

下面的例子有助于理解Relaxed Binding。
假设有一个带@ConfigurationProperties注解的属性类

@ConfigurationProperties(prefix="my")
public class MyProperties {
    private String firstName;
    public String getFirstName() {
        return this.firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
}

在配置文件(properties文件)中,以下的写法都能将值正确注入到firstName这个属性中。

property 写法 说明 推荐场景
my.firstName 标准驼峰
my.first-name 减号分隔 推荐在properties或yml文件中使用
my.first_name 下划线分隔
MY_FIRST_NAME 大写+下划线分隔 推荐用于环境变量或命令行参数

源码分析

出于好奇,我研究了一下SpringBoot的源码,发现在1.5版本与2.0版本中,Relaxed Binding 的具体实现是不一样的。
为了解释两个版本的不同之处,我首先在properties配置文件中做如下的配置。

my.first_Name=fonoisrev

Spring Boot 1.5版本的实现(以1.5.14.RELEASE版本为例)

属性与配置值的绑定逻辑始于ConfigurationPropertiesBindingPostProcessor类的postProcessBeforeInitialization函数。

public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
        BeanFactoryAware, EnvironmentAware, ApplicationContextAware, InitializingBean,
        DisposableBean, ApplicationListener<ContextRefreshedEvent>, PriorityOrdered {
...
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        ...
        ConfigurationProperties annotation = AnnotationUtils
                .findAnnotation(bean.getClass(), ConfigurationProperties.class);
        if (annotation != null) {
            postProcessBeforeInitialization(bean, beanName, annotation);
        }
        ...
    }

    private void postProcessBeforeInitialization(Object bean, String beanName,
            ConfigurationProperties annotation) {
        Object target = bean;
        PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
                target);
        factory.setPropertySources(this.propertySources);
        ...
        try {
            factory.bindPropertiesToTarget();
        }
        ...
    }
...
}

从以上代码可以看出,该类实现了BeanPostProcessor接口(BeanPostProcessor官方文档请点我)。BeanPostProcessor接口主要作用是允许自行实现Bean装配的逻辑,而postProcessBeforeInitialization函数则在执行Bean的初始化调用(如afterPropertiesSet)前被调用。

postProcessBeforeInitialization函数执行时,属性值绑定的工作被委派给了PropertiesConfigurationFactory<T>类。

public class PropertiesConfigurationFactory<T> implements FactoryBean<T>,
        ApplicationContextAware, MessageSourceAware, InitializingBean {
...
    public void bindPropertiesToTarget() throws BindException {
        ...
            doBindPropertiesToTarget();
        ...
    }
    private void doBindPropertiesToTarget() throws BindException {
        RelaxedDataBinder dataBinder = (this.targetName != null
                ? new RelaxedDataBinder(this.target, this.targetName)
                : new RelaxedDataBinder(this.target));
        ...
        Iterable<String> relaxedTargetNames = getRelaxedTargetNames();
        Set<String> names = getNames(relaxedTargetNames);
        PropertyValues propertyValues = getPropertySourcesPropertyValues(names,
                relaxedTargetNames);
        dataBinder.bind(propertyValues);
        ...
    }

    private PropertyValues getPropertySourcesPropertyValues(Set<String> names,
            Iterable<String> relaxedTargetNames) {
        PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names,
                relaxedTargetNames);
        return new PropertySourcesPropertyValues(this.propertySources, names, includes,
                this.resolvePlaceholders);
    }
...
}

以上的代码其逻辑可以描述如下:

  1. 借助RelaxedNames类,将注解@ConfigurationProperties(prefix="my")中的前缀“my”与MyProperties类的属性firstName可能存在的情况进行穷举(共28种情况,如下表)
序号 RelaxedNames
0 my.first-name
1 my_first-name
2 my.first_name
3 my_first_name
4 my.firstName
5 my_firstName
6 my.firstname
7 my_firstname
8 my.FIRST-NAME
9 my_FIRST-NAME
10 my.FIRST_NAME
11 my_FIRST_NAME
12 my.FIRSTNAME
13 my_FIRSTNAME
14 MY.first-name
15 MY_first-name
16 MY.first_name
17 MY_first_name
18 MY.firstName
19 MY_firstName
20 MY.firstname
21 MY_firstname
22 MY.FIRST-NAME
23 MY_FIRST-NAME
24 MY.FIRST_NAME
25 MY_FIRST_NAME
26 MY.FIRSTNAME
27 MY_FIRSTNAME
  1. 构造PropertySourcesPropertyValues对象从配置文件(如properties文件)中查找匹配的值,
  2. 使用DataBinder操作PropertySourcesPropertyValues实现MyProperties类的setFirstName方法调用,完成绑定。
public class PropertySourcesPropertyValues implements PropertyValues {
...
    PropertySourcesPropertyValues(PropertySources propertySources,
            Collection<String> nonEnumerableFallbackNames,
            PropertyNamePatternsMatcher includes, boolean resolvePlaceholders) {
        ...
        PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver(
                propertySources);
        for (PropertySource<?> source : propertySources) {
            processPropertySource(source, resolver);
        }
    }

    private void processPropertySource(PropertySource<?> source,
            PropertySourcesPropertyResolver resolver) {
        ...
            processEnumerablePropertySource((EnumerablePropertySource<?>) source,
                    resolver, this.includes);
        ...
    }

    private void processEnumerablePropertySource(EnumerablePropertySource<?> source,
            PropertySourcesPropertyResolver resolver,
            PropertyNamePatternsMatcher includes) {
        if (source.getPropertyNames().length > 0) {
            for (String propertyName : source.getPropertyNames()) {
                if (includes.matches(propertyName)) {
                    Object value = getEnumerableProperty(source, resolver, propertyName);
                    putIfAbsent(propertyName, value, source);
                }
            }
        }
    }
...
}

从以上代码来看,整个匹配的过程其实是在PropertySourcesPropertyValues对象构造的过程中完成的。
更具体地说,是在DefaultPropertyNamePatternsMatcher的matches函数中完成字符串匹配的,如下代码。

class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
...
    public boolean matches(String propertyName) {
        char[] propertyNameChars = propertyName.toCharArray();
        boolean[] match = new boolean[this.names.length];
        boolean noneMatched = true;
        for (int i = 0; i < this.names.length; i++) {
            if (this.names[i].length() <= propertyNameChars.length) {
                match[i] = true;
                noneMatched = false;
            }
        }
        if (noneMatched) {
            return false;
        }
        for (int charIndex = 0; charIndex < propertyNameChars.length; charIndex++) {
            for (int nameIndex = 0; nameIndex < this.names.length; nameIndex++) {
                if (match[nameIndex]) {
                    match[nameIndex] = false;
                    if (charIndex < this.names[nameIndex].length()) {
                        if (isCharMatch(this.names[nameIndex].charAt(charIndex),
                                propertyNameChars[charIndex])) {
                            match[nameIndex] = true;
                            noneMatched = false;
                        }
                    }
                    else {
                        char charAfter = propertyNameChars[this.names[nameIndex]
                                .length()];
                        if (isDelimiter(charAfter)) {
                            match[nameIndex] = true;
                            noneMatched = false;
                        }
                    }
                }
            }
            if (noneMatched) {
                return false;
            }
        }
        for (int i = 0; i < match.length; i++) {
            if (match[i]) {
                return true;
            }
        }
        return false;
    }

    private boolean isCharMatch(char c1, char c2) {
        if (this.ignoreCase) {
            return Character.toLowerCase(c1) == Character.toLowerCase(c2);
        }
        return c1 == c2;
    }

    private boolean isDelimiter(char c) {
        for (char delimiter : this.delimiters) {
            if (c == delimiter) {
                return true;
            }
        }
        return false;
    }
}

如上代码,将字符串拆为单个字符进行比较,使用了双层循环匹配。

Spring Boot 2.0版本的实现(以2.0.4.RELEASE版本为例)

与1.5.14.RELEASE有很大区别。
属性与配置值的绑定逻辑依旧始于
ConfigurationPropertiesBindingPostProcessor 类的
postProcessBeforeInitialization 函数。
但 ConfigurationPropertiesBindingPostProcessor 类的定义与实现均发生了变化。先看代码。

public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
        PriorityOrdered, ApplicationContextAware, InitializingBean {
...
    private ConfigurationPropertiesBinder configurationPropertiesBinder;
...
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        ConfigurationProperties annotation = getAnnotation(bean, beanName,
                ConfigurationProperties.class);
        if (annotation != null) {
            bind(bean, beanName, annotation);
        }
        return bean;
    }

    private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
        ResolvableType type = getBeanType(bean, beanName);
        Validated validated = getAnnotation(bean, beanName, Validated.class);
        Annotation[] annotations = (validated != null)
                ? new Annotation[] { annotation, validated }
                : new Annotation[] { annotation };
        Bindable<?> target = Bindable.of(type).withExistingValue(bean)
                .withAnnotations(annotations);
        try {
            this.configurationPropertiesBinder.bind(target);
        }
        ...
    }
...
}

从以上代码可以看出,该类依旧实现了BeanPostProcessor接口,但调用postProcessBeforeInitialization函数,属性值绑定的工作则被委派给了ConfigurationPropertiesBinder类,调用了bind函数。

class ConfigurationPropertiesBinder {
...
    public void bind(Bindable<?> target) {
        ConfigurationProperties annotation = target
                .getAnnotation(ConfigurationProperties.class);
        Assert.state(annotation != null,
                () -> "Missing @ConfigurationProperties on " + target);
        List<Validator> validators = getValidators(target);
        BindHandler bindHandler = getBindHandler(annotation, validators);
        getBinder().bind(annotation.prefix(), target, bindHandler);
    }

    private Binder getBinder() {
        if (this.binder == null) {
            this.binder = new Binder(getConfigurationPropertySources(),
                    getPropertySourcesPlaceholdersResolver(), getConversionService(),
                    getPropertyEditorInitializer());
        }
        return this.binder;
    }
...
}

ConfigurationPropertiesBinder类并不是一个public的类。实际上这相当于ConfigurationPropertiesBindingPostProcessor的一个内部静态类,表面上负责处理@ConfigurationProperties注解的绑定任务。
但从代码中可以看出,具体的工作委派给另一个Binder类的对象。
Binder类是SpringBoot 2.0版本后加入的类,其负责处理对象与多个ConfigurationPropertySource之间的绑定。

public class Binder {
...
    private static final List<BeanBinder> BEAN_BINDERS;
    static {
        List<BeanBinder> binders = new ArrayList<>();
        binders.add(new JavaBeanBinder());
        BEAN_BINDERS = Collections.unmodifiableList(binders);
    }
...
    public <T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handler) {
        return bind(ConfigurationPropertyName.of(name), target, handler);
    }

    public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target,
            BindHandler handler) {
        ...
        Context context = new Context();
        T bound = bind(name, target, handler, context, false);
        return BindResult.of(bound);
    }

    protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
            BindHandler handler, Context context, boolean allowRecursiveBinding) {
        ...
        try {
            ...
            Object bound = bindObject(name, target, handler, context,
                    allowRecursiveBinding);
            return handleBindResult(name, target, handler, context, bound);
        }
        ...
    }

    private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target,
            BindHandler handler, Context context, boolean allowRecursiveBinding) {
        ConfigurationProperty property = findProperty(name, context);
        if (property == null && containsNoDescendantOf(context.streamSources(), name)) {
            return null;
        }
        AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);
        if (aggregateBinder != null) {
            return bindAggregate(name, target, handler, context, aggregateBinder);
        }
        if (property != null) {
            try {
                return bindProperty(target, context, property);
            }
            catch (ConverterNotFoundException ex) {
                // We might still be able to bind it as a bean
                Object bean = bindBean(name, target, handler, context,
                        allowRecursiveBinding);
                if (bean != null) {
                    return bean;
                }
                throw ex;
            }
        }
        return bindBean(name, target, handler, context, allowRecursiveBinding);
    }

    private Object bindBean(ConfigurationPropertyName name, Bindable<?> target,
            BindHandler handler, Context context, boolean allowRecursiveBinding) {
        ...
        BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(
                name.append(propertyName), propertyTarget, handler, context, false);
        Class<?> type = target.getType().resolve(Object.class);
        ...
        return context.withBean(type, () -> {
            Stream<?> boundBeans = BEAN_BINDERS.stream()
                    .map((b) -> b.bind(name, target, context, propertyBinder));
            return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);
        });
    }
...
    private ConfigurationProperty findProperty(ConfigurationPropertyName name,
            Context context) {
        ...
        return context.streamSources()
                .map((source) -> source.getConfigurationProperty(name))
                .filter(Objects::nonNull).findFirst().orElse(null);
    }
}

如下代码,Binder类中实现了一个比较复杂的递归调用。

  1. ConfigurationPropertiesBinder 调用Binder类的bind函数时,参数通过层层转换,来到bindObject函数中。
  2. bindObject函数中,通过bindAggregate,bindProperty与bindBean等私有方法逐步推导绑定,bindBean是最后一步。
  3. bindBean函数通过定义BeanPropertyBinder的lambda表达式,允许bean绑定过程递归调用bindObject函数。

实际上,bindObject函数中findProperty函数调用是从properties文件中查找匹配项的关键,根据lambda表达式的执行结果,properties配置文件的配置项对应的是SpringIterableConfigurationPropertySource类型,因此调用的是其getConfigurationProperty函数,关键代码如下。

class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource
        implements IterableConfigurationPropertySource {
...
    @Override
    public ConfigurationProperty getConfigurationProperty(
            ConfigurationPropertyName name) {
        ConfigurationProperty configurationProperty = super.getConfigurationProperty(
                name);
        if (configurationProperty == null) {
            configurationProperty = find(getPropertyMappings(getCache()), name);
        }
        return configurationProperty;
    }

    protected final ConfigurationProperty find(PropertyMapping[] mappings,
            ConfigurationPropertyName name) {
        for (PropertyMapping candidate : mappings) {
            if (candidate.isApplicable(name)) {
                ConfigurationProperty result = find(candidate);
                if (result != null) {
                    return result;
                }
            }
        }
        return null;
    }
...
}

最终,isApplicable函数中判断properties文件中的配置项(my.first_Name)与MyProperties类中的属性(firstName)是否匹配,其字符串比较过程使用的是ConfigurationPropertyName类重写的equals方法。

public final class ConfigurationPropertyName
        implements Comparable<ConfigurationPropertyName> {
    @Override
    public boolean equals(Object obj) {
        ...
        ConfigurationPropertyName other = (ConfigurationPropertyName) obj;
        ...
        for (int i = 0; i < this.elements.length; i++) {
            if (!elementEquals(this.elements[i], other.elements[i])) {
                return false;
            }
        }
        return true;
    }

    private boolean elementEquals(CharSequence e1, CharSequence e2) {
        int l1 = e1.length();
        int l2 = e2.length();
        boolean indexed1 = isIndexed(e1);
        int offset1 = indexed1 ? 1 : 0;
        boolean indexed2 = isIndexed(e2);
        int offset2 = indexed2 ? 1 : 0;
        int i1 = offset1;
        int i2 = offset2;
        while (i1 < l1 - offset1) {
            if (i2 >= l2 - offset2) {
                return false;
            }
            char ch1 = indexed1 ? e1.charAt(i1) : Character.toLowerCase(e1.charAt(i1));
            char ch2 = indexed2 ? e2.charAt(i2) : Character.toLowerCase(e2.charAt(i2));
            if (ch1 == '-' || ch1 == '_') {
                i1++;
            }
            else if (ch2 == '-' || ch2 == '_') {
                i2++;
            }
            else if (ch1 != ch2) {
                return false;
            }
            else {
                i1++;
                i2++;
            }
        }
        while (i2 < l2 - offset2) {
            char ch = e2.charAt(i2++);
            if (ch != '-' && ch != '_') {
                return false;
            }
        }
        return true;
    }
}

两者的异同

总结一下两个版本实现的不同。

对比科目 Spring Boot-1.5.14.RELEASE版本 Spring Boot-2.0.4.RELEASE版本
java版本 jdk1.7以上 jdk1.8以上
关键类 ConfigurationPropertiesBindingPostProcessor,PropertiesConfigurationFactory<T>,PropertySourcesPropertyValues,DefaultPropertyNamePatternsMatcher ConfigurationPropertiesBindingPostProcessor,ConfigurationPropertiesBinder,SpringIterableConfigurationPropertySource,ConfigurationPropertyName
查找方式 将属性拼接为字符串,穷举字符串的可能性 递归查找,分级匹配
字符串匹配方式 双层循环匹配 单层循环匹配
代码风格 传统java代码 大量使用lambda表达式
优点 由于使用传统代码风格,代码层次相对简单,可读性较强 不采用穷举的方式,匹配更精准,查找更有效
缺点 由于使用拼接穷举,当属性数量大且有“-”或“_”分隔单词时,组合会变多,影响查找效率 使用lambda表达式,产生各种延迟绑定,代码可读性差,不易调试

题外话:为什么我发现了这两个版本实现的不一致?

前一两个月,我写了一个自动识别消息的框架,在消息中模糊查找可能存在的关键信息,用了1.5版本的RelaxedNames类,虽然这个类在官方文档上没有提到,但是挺好用的。
然而最近,我想把框架迁移到2.0版本上,结果遇到了编译错误。因此特别研究了一下,发现这些类都不见了。
幸好只是小问题,我把RelaxedNames源码拷贝过来一份,就解决了问题,但这也给我提了个醒,大版本升级还是要仔细阅读下代码,避免留下一些坑。
另外也从一个侧面反映出,Spring Boot的2.0版本相比于1.5确实做了不小的更改,有空再好好琢磨下。

相关文章
|
1天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
9 2
|
1天前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
9 1
|
3天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
11 2
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
52 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
17天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
24 1
|
18天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
24 1
|
1月前
|
缓存 JavaScript Java
Spring之FactoryBean的处理底层源码分析
本文介绍了Spring框架中FactoryBean的重要作用及其使用方法。通过一个简单的示例展示了如何通过FactoryBean返回一个User对象,并解释了在调用`getBean()`方法时,传入名称前添加`&`符号会改变返回对象类型的原因。进一步深入源码分析,详细说明了`getBean()`方法内部对FactoryBean的处理逻辑,解释了为何添加`&`符号会导致不同的行为。最后,通过具体代码片段展示了这一过程的关键步骤。
Spring之FactoryBean的处理底层源码分析
|
29天前
|
架构师 Java 开发者
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
在40岁老架构师尼恩的读者交流群中,近期多位读者成功获得了知名互联网企业的面试机会,如得物、阿里、滴滴等。然而,面对“Spring Boot自动装配机制”等核心面试题,部分读者因准备不足而未能顺利通过。为此,尼恩团队将系统化梳理和总结这一主题,帮助大家全面提升技术水平,让面试官“爱到不能自已”。
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
|
15天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
19 0
|
1月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
56 2