快递100 是专业的快递物流互联网平台,为数以亿计的用户提供快递物流的查、寄件服务。快递100 的各种服务已经稳定高效的运行了多年,为中国的快递物流行业的快速发展做出了很大的贡献,这一切都离不开背后默默贡献的程序猿们。
从第一个独立站点上线至今,快递100 已经走过了10个年头,10年沧海桑田,10年经历了一次又一次的技术、架构升级。近年来,由于spring-boot、spring-cloud技术的兴起,快递100 技术团队也开始对后端服务进行重构,基于spring-boot、spring-cloud技术,对原有的系统进行微服务化改造。在改造的前期,由于许多开发人员对spring系列的技术缺少系统性的认知,所以我们编辑整理了一个spring系列技术使用的手册,帮助开发人员可以快速的上手。
1. Spring表达式(Spring EL)
Spring EL是指Spring框架表达式语言,这是从Spring 3开始支持的一种用于在运行时通过表达式来读取、运算得到需要的值,将其装配到Bean中的技术。Spring EL的引入,使Spring框架可以支持根据运行时计算的结果来进行动态的注入,有别于单纯根据注解或者Xml配置文件配置的方式进行的静态注入,使Spring拥有更强大的注入功能。
在实际使用,我们主要是搭配@Value注解在Bean的属性、setter方法或者构造方法参数上进行标注,或者通过Xml配置Bean的时候配置属性或构造方法参数时使用。Spring EL有非常强大的功能, 下面列举了Spring EL常用的一些场景和对应的用法:
- 占位符:使用${…}运算符,表明需要从配置文件(properties文件或者yml文件)中读取大括号中指定的配置项的值,注入到Bean的属性或者构造方法参数中,这是最常见的一种使用场景,下面的代码片段表示在做数据库数据源配置时,从属性配置文件中获取url、userName和password对应的配置项的值,注入到数据源配置Bean中
@Component
public class DatabaseProperties {
@Value("${database.url}")
private String url;
@Value("${database.username}")
private String userName;
@Value("${database.password}")
private String password;
}
- 注入简单值:通过:#{value}的方式,给Bean注入value所代表的简单的数值
@Component
public class DemolProperties {
@Value("#{3000}")
private long maxWait;
@Value("#{localhost}")
private String host;
@Value("#{3.14}")
private float pi;
}
- 还可以通过#{beanName.property}获取其他Bean的属性值注入到Bean中,其中beanName是其他Bean装配到IoC容器中的名字,property是Bean中属性的名称
@Component
public class StudentProperties {
@Value("#{student.name}")
private String studentName;
}
- 调用方法:可以通过#{beanName.methodName()}的方式调用方法:
@Value("#{teacher.showName()}")
private String teacherName;
上面的代码片段表示调用名称为teacher的Bean的showName的无参方法,如果是调用有参数的方法,则可以在方法调用中加入参数:
@Value("#{teacher.showName('history')}")
private String teacherName;
也可以调用其他的方法。假如上面示例中teacher.showName()返回值为字符串,可以通过Spring EL,将其转换成大写字母,然后注入到Bean中:
@Value("#{teacher.showName().toUpperCase()}")
private String teacherName;
还可以通过?操作符,先判断teacher.showName()返回值是否为Null,如果是,则不进行转换操作:
@Value("#{teacher.showName()?.toUpperCase()}")
private String teacherName;
- 引用静态属性或调用静态类方法:通过#{T(className).propertyName}或者#{T(className).methodName}可以引用静态属性或者调用静态方法,其中className表示静态类的类名,propertyName表示静态属性名,methodName表示静态方法名:
@Component
public class TeacherProperties {
@Value("#{T(springboot.demo.constant.Constant).SUBJECT_HISTORY}")
private String subject;
@Value("#{T(springboot.demo.constant.Constant).getSubjectTeacher('history')}")
private String subjectTeacher;
}
- 运算: Spring EL可以支持各种数值运算,如字符串拼接、数值计算、逻辑运算、使用正则表达式计算等。下面的代码片段分别提供了几种常见的通过Spring EL进行运算的示例:
@Value("#{grade.name + ' ' + class.name }")
private String className;
@Value("#{3 * T(java.lang.Math).PI}")
private double result;
@Value("#{framework.test == 1 ? 'testSchoolApp' : 'schoolApp'}")
private String appName;
@Value("#{email.url match '\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+'}")
private boolean regularEmail;
- 访问数组、列表和Map:Spring EL也可以访问数组、列表和Map类型的属性,获取对应的元素并将其注入到Bean中。数组和集合通过 “[下标]”的方式获取下标对应的元素,Map通过“[key]”的方式获取key对应的元素:
@Value("#{constant.demoList[0]}")
private String listElement;
@Value("#{constant.demoMap['key1']}")
private String mapElement;
- 筛选集合和投影:Spring EL支持对访问的集合进行筛选和投影。筛选是通过在[]中加入筛选条件,过滤得到满足条件的元素注入到Bean中,而投影则是对满足过滤条件的元素,只选择其中的一个属性值注入到Bean中。筛选时可以使用?运算符,表示获取满足条件的所有元素;使用^运算符,表示获取满足筛选条件的第一个元素;使用$运算符,表示获取满足筛选条件的最后一个元素:
//从列表中获取所有满足条件的元素注入到Bean中
@Value("#{constant.subjects.?[grade == 2]}")
private List<Subject> subjects;
//从列表中获取满足条件的第一个元素注入到Bean中
@Value("#{constant.subjects.^[grade == 2]}")
private Subject firstSubject;
//从列表中获取满足条件的最后一个元素注入到Bean中
@Value("#{constant.subjects.$[grade == 2]}")
private Subject lastSubject;
//从列表中获取满足条件的元素,并进行投影,将其name属性注入到Bean中
@Value("#{constant.subjects.?[grade == 2].![name]}")
private List<String> subjectNames;
2. 配置文件配置项注入
在spring boot应用中,可以通过@Value注解标注Bean的属性,通过占位符的方式从application.properties(或application.yml)中获取对应的配置值,注入到Bean中:
@Value("#{database.url}")
private String url;
@Value("#{database.username}")
private String username;
@Value("#{database.password}")
private String password;
此外也可以通过@ConfigurationProperties注解对类进行标注,@ConfigurationProperties注解需要制定prefix配置,标注此注解后框架会从application.properties或application.yml查找配置值,将其注入到类中的属性中,查找的方式为prefix+“.”+属性的全限定名,如上面的配置可以修改为:
@Component
@ConfigurationProperties(prefix = 'databse')
public class DatabaseProperties {
private String url;
private String username;
private String password;
}
修改之后,框架会按照database.url、database.userName、database.password的方式查找配置值并注入到Bean中。如果Bean的属性为对象类型,则会通过:prefix+“.”+属性名+“.”+属性对象的属性名 这样的路径去查找和注入配置值。如果上面的例子中,数据库连接配置项更改为通过一个ConnectionProperties类进行封装(代码片段中使用了lombox组件的@Data标签,自动生成getter、setter等方法,用来简化pojo类):
@Data
public class ConnectionProperties {
private String url;
private String username;
private String password;
}
则数据源配置类修改为:
@Data
@Component
@ConfigurationProperties(prefix = "databse")
public class DatabaseProperties {
private ConnectionProperties connect;
}
框架在注入配置值时,会首先创建一个ConnectionProperties类的实例,然后按照database.connect.url、database.connect.userName、database.connect.password的方式获取配置值,注入到ConnectionProperties实例中,然后将这个实例注入到DataSourceConfig Bean中。
3. 载入属性文件
对于application.properties(包含application-{profile}.properties)之外的属性文件,可以使用@PropertySource注解进行注入。@PropertySource注解需要通过value配置指定属性文件的路径,框架会按照指定的路径去装载属性文件,被装载的属性文件,可以通过上述的方式,注入到Bean中。
@SpringBootApplication
@Properties("classpath:demo.properties")
public class DemoMain {
public static void main(String[]args) {
new SpringApplicationBuilder(DemoMain.class).run(args);
}
}
4. Spring boot常用注解
Spring boot框架鼓励用注解来进行程序参数配置、IoC装配配置等,因此框架提供了大量的注解,用于进行配置、装配、注入等操作,这部分主要介绍关于bean的装配、注入和程序配置等方面的注解。除此之外,Spring框架还包含大量的其他注解,如AOP注解、Spring MVC注解等等,这些注解我们放到对应的内容中进行介绍,而不在此处一一列举。
4.1 @SpringBootApplication
@SpringBootApplication用于标注spring boot应用的启动类,表示这是spring boot应用的入口。@SpringBootApplication注解实际上包含了@EnableAutoConfiguration、@Configuration和@ComponentScan三个注解,@SpringBootApplication注解中包含的配置及其说明如下:
- exclude:指定需要排除扫描的类型数组,该数组包含的类型即使是在@ComponentScan注解扫描的范围内,也不会被框架扫描和装配,但需要注意的是,这个配置是针对@EnableAutoConfiguration注解的,也就是说,只有配置的类型是使用@Configuration标注的java配置类,才会被排除
- excludeName:跟exclude配置实质是相同的,只是指定排除的类型是通过类的全限定名来指定,主要用于需要排除的类型是一些不一定包含在类路径中的第三方类时使用。与exclude相同的是,这个配置也只是对使用@Configuration标注的java配置类才会生效
- scanBasePackages:此配置是为@ComponentScan注解指定basePackages配置,用于指定框架扫描装配Bean的基础路径。默认情况下此配置为空,框架会自动扫描装配与@SpringBootApplication注解标注的类所在的包及其子包下的类进行装配
- scanBasePackageClasses:此配置是为@ComponentScan注解指定basePackageClasses配置,让框架在指定的类或接口所属的包为基础路径进行扫描和装配
4.2 @ComponentScan注解
@ComponentScan注解用于指定框架扫描和装配Bean的策略,可以标注在使用@SpringBootApplication标注的启动类或者使用@Configuration标注的java配置类中,让框架按照指定的策略去扫描和加载Bean。
@ComponentScan注解包含的配置及其描述如下:
- basePackages和value:指定扫描和装配Bean的基础包路径,框架会扫描和装配指定的包及其子包中包含@Component注解的类。默认值为空,表示扫描和装配被@ComponentScan注解标注的类所在的包及其子包下的类
- basePackageClasses:类型数组配置,让框架扫描数组中包含的类或接口所在的包及其子包下包含@Component注解标注的类进行装配
- nameGenerator:指定Bean的名称生成器类型,当Bean的定义中未显式指定名称时,通过名称生成器生成名称。默认值为BeanNameGenerator接口类型,表示使用IoC容器默认的名称生成器。应用中可以配置一个BeanNameGenerator接口的实现类,框架在装配Bean时会使用配置的名称生成器实现类来给Bean生成名称
- scopeResolver:指定scopre解析器,用于将@Scope注解所描述的Bean的作用域配置解析成为ScopeMetadata对象。默认值为AnnotationScopeMetadataResolver
- scopedProxy:指定scope代理模式,配置值为ScopeProxyMode枚举值,有四个可选项:DEFAULT(默认值,表示用容器默认的模式)、NO(不创建代理)、INTERFACES(基于接口创建代理)、TARGET_CLASS(基于类创建代理)。当容器需要将因为不在其作用域而尚未创建的Bean注入到其他对其有依赖关系的Bean时,就可能需要通过代理模式,注入一个代理的Bean。比如有Bean为A,其作用域是singleton,依赖于一个作用域为request的Bean B,A的作用域为singleton,需要在容器启动时就完成初始化,需要注入B,而B的作用域是request,在容器启动时并没有创建,这时候就需要创建一个B的代理Bean,将其注入到A中。此配置项就是用于在创建代理Bean时,指定创建代理的模式
- resourcePattern:在扫描装配Bean时,对资源进行筛选的正则表达式。这是为资源定位指定一个大的范畴,具体的过滤和筛选的细节在includeFilters和excludeFilters中进行配置
- useDefaultFilters:是否开启对@Component、@Repository、@Service、@Controller、
- includeFilters:对扫描范围内的资源进行筛选的过滤器数组设置,对于满足过滤条件的资源,框架会对其进行解析和装配
- excludeFilters:对于扫描范围内的资源进行过滤的过滤器数组设置,对满足条件的资源,框架会将其排除,不会进行解析和装配
- lazyInit:是否设置延迟加载,如果设置为true,则框架扫描和装配的Bean,不会在容器启动时进行初始化,而会延迟到Bean第一次使用时进行初始化。默认值为false
4.3 @Import注解
@Import注解与Xml配置中<import />标签作用一致,用于引入其他使用@Configuration注解标注的java配置类、ImportSelector接口实现类、ImportBeanDefinitionRegistrar和普通使用@Component注解标注的类。
当@Import标签引入的类是@Configuration注解标注的类时,框架会将这个类当做java配置类进行处理,将其进行装配,同时解析该类中包含@Bean的方法,装配其配置的Bean。如果引入的类是使用@Component注解标注的类,则会将其当做普通的Bean进行装配。
如果@Import标签引入的类是ImportSelector接口或者ImportBeanDefinitionRegistrar接口的实现类,则不会将其进行装配,而只会调用其方法,来处理标注@Import注解的配置类中按照自定义的条件选择需要导入的类(ImportSelector)或完成Bean的注册(ImportBeanDefinitionRegistrar)。
ImportSelector接口有一个子接口DeferredImportSelector,两者的区别,是在被@Import标签引用时,ImportSelector接口的实现类的selectImports方法会在@Configuration标注的类的其他注解如@ImportSource、@Bean等被处理逻辑执行之前执行,而DeferredImportSelector接口的实现类的selectImports方法则是在这些处理逻辑执行之后执行。框架会根据selectImports方法返回的类名,将对应的类导入进行装配。
4.4 @EnableAutoConfiguration注解
@EnableAutoConfiguration注解标注应用启动自动配置支持,该注解中通过@Import注解,引入了一个ImportSelector接口的实现类:AutoConfigurationImportSelector,该类实现的selectImports方法,会加载classpath下包括第三方jar包在内的/META-INF/spring.factories文件,读取其中org.springframework.boot.autoconfigue.EnableAutoConfiguration对应的配置信息,解析出其中指定的配置类,经过过滤处理后将其返回,框架会将返回的类进行装配,如果这些类使用@Configuration注解标注,则会将其作为普通java配置类进行处理,对其进行解析,装配其中配置的Bean,由此实现自动配置的功能。
如果我们想让自己实现的一个组件,也能够基于spring boot实现自动配置的功能,在应用中加入依赖时可以自动被框架扫描和装配其中的配置类,就可以在resources目录下添加一个META-INF目录,在其中增加一个spring.factories文件,文件中配置:org.springframework.boot.autoconfigue.EnableAutoConfiguration=需要自动装配的配置类的全限定名,这样当应用依赖了这个组件之后,在应用启动时,IoC容器就会加载配置中指定的配置类,将其配置的Bean进行装配。如果有多个配置类的话,可以使用“,”进行分隔。下图是spring boot框架的spring-boot-autoconfigure组件的spring.factories文件中自动配置的片段。其中“\”符号用于进行换行转义。
4.5 @Configuration注解
@Configuration用于将类标注为配置类,用来替代通过Xml来配置Bean描述的方式。容器启动时,会注册一个BeanFacotoryProcessor接口的实现类:ConfigurationClassPostProcessor,这是BeanFactory的后置处理器,用于在BeanFactory实例创建之后的后置处理,扫描到包含@Configuration注解的类时,会将其装配,并且会解析这个类,处理其对应的注解,将其配置的其他Bean进行装配。
@Configuration注解本身标注了@Component注解,这表明使用@Configuration标注的类,也能像标注@Component注解的类一样,能够被框架扫描和装配。@Configuration注解只有一个value配置项,这是为其标注的@Component注解提供的,用于在IoC容器装配所标注的类的Bean时,作为Bean的名称,默认值为空,表示由框架指定的名称生成器生成Bean的名,默认情况下会将类名第一个字符转变为小写字母后作为Bean的名称。
4.6 @ConfigurationProperties注解和@EnableConfigurationProperties注解
@ConfigurationProperties注解用来标注类,指明类可以通过注入配置属性值,来填充对应的属性。对应的配置说明请参见前面配置文件配置项注入部分的内容,@ConfigurationProperties注解包含的配置及其说明如下:
- prefix和value:指定配置属性的前缀,最后将配置属性注入到Bean中时,通过:前缀+类属性全限定名 的方式进行查找和注入
- ignoreInvalidFields:是否忽略无法转换的字段。如果与类中属性对应的一个配置属性有值,但无法转换成类属性需要的类型,这个配置决定是否对其忽略,如果不忽略,则框架会抛出一个类型转换错误导致应用不能启动,如果忽略,则不会注入该字段,但应用可以正常启动。默认情况下是true
- ignoreUnknownFields:是否忽略未知的字段。当配置文件中包含满足前缀条件而类中没有名称与之匹配的属性时,这个配置决定是否对其忽略,如果忽略,则框架不会进行注入,否则框架会抛出一个异常导致应用不能正常启动。默认情况下是false
@ConfigurationProperties注解本身没有标注@Component注解,因此如果只标注@ConfigurationProperties注解的话,默认情况下是不会被框架扫描和装配的,如果需要框架对该类进行装配,一种方式是给该类同时标注@Component注解(或包含@Component注解的其他注解),另一种方式,则是可以配合@EnableConfigurationProperties注解使用,在使用到这个类的其他配置类中,加上@EnableConfigurationProperties注解,并在其value中配置这个类的类型,然后给标注的类加上一个该类型的属性(或者定义带该类型参数的构造方法),使用@Autowired注解进行注入。
@Configuration
@EnableConfigurationProperties({ConnectionProperties.class})
public class AppConfig {
@Autowired
private ConnectionProperties connectionProperties;
}
如上面的代码片段所示,在AppConfig类中,标注了@EnableConfigurationProperties,value中配置了ConnectionProperties类型,并在AppConfig类中声明了一个ConnectionProperties类型的属性,使用@Autowired注解进行标注。这样,框架在扫描装配AppConfig时,因为@EnableConfigurationProperties注解通过@Import注解引入了一个ImportSelector接口的实现类:EnableConfigurationPropertiesImportSelector,该类会将@EnableConfigurationProperties注解的value中配置的类型提交给框架进行装配,因此会将ConnectionProperties类进行装配,装配之后就可以通过@Autowired注入到AppConfig中了。
4.7 @PropertySource注解
@PropertySource注解用于标注类,让框架加载指定的属性文件,通常跟@Value或@ConfigurationProperties注解配合使用,将指定的属性文件中的配置加载到配置类中。
@PropertyValue注解包含的配置项如下:
- name:属性源的名称,默认为空
- value:配置属性文件的路径,这是一个数组类型的配置,可以配置多个文件路径
- ignoreResourceNotFound:是否忽略找不到属性文件的情况,如果value中配置的属性文件访问路径存在无法访问的情况,此配置决定框架是否要抛出异常。默认值为false
- encoding:读取属性文件时采用的编码方式,默认值为空,表示采用系统默认的编码
- factory:配置属性源工厂类。此配置项为PropertySourceFactory接口实现类的类型参数,指定用于读取属性文件数据创建PropertySource对象的工厂类实现
4.8 @ImportSource注解
@ImportSource用于引入spring的Xml配置文件配置的Bean。通过@ImportSource标签,我们可以在基于注解的spring配置中,支持Xml形式配置的Bean。
@ImportSource注解包含的配置及其说明如下:
- locations和value:用于配置Xml配置文件的路径,这是数组类型的配置,可以支持配置多个Xml文件访问路径
- reader:配置Xml配置文件读取器,此配置值是BeanDefinitionReader接口的实现类类型参数,用于指定容器读取Xml文件,解析生成BeanDefinition所使用的读取器的实现,默认值为BeanDefinitionReader.class,即让容器使用默认的读取器
4.9 @Component注解
@Component注解用于标注类,告知框架该类是需要进行解析和装配的。默认情况下,当标注@Component的类在框架扫描范文内(基于@ComponentScan注解配置的扫描策略或者Xml配置文件中配置的扫描策略)时,框架就会将这个类进行解释和装配。
@Component注解只有一个配置项value,这是字符串类型的配置项,用于指定扫描装配的Bean的名称。默认值为空,表示不显式指定Bean名称,让容器使用指定的BeanNameGeneror实现为装配的Bean生成名字。默认的名字生成器会将类名简写中第一个字母转变为小写字母,作为Bean的名称。
在spring和spring boot框架中,@Component注解有许多标注了这个注解的变体,如@Service、@Repository、@Controller、@Configuration等,默认情况下,标注这些注解的类,如果在框架扫描范围内,也会被框架进行装配。
4.10 @Autowired注解
@Autowired注解可以用来标注属性、setter方法、构造方法和方法参数,表示被标注的这些属性、setter方法对应的属性和构造方法中的参数等,可以从IoC容器中查找与之匹配的Bean来进行注入,完成自动装配。查找的规则首先是基于类型进行查找,从IoC容器中找到与属性、参数类型最相匹配的Bean,如果找到的Bean只有一个,直接将其注入,如果有多个,则会从中查找名称与之匹配的Bean进行注入,如果再多个类型匹配的Bean中无法找到名称匹配的Bean,则需要借助@Primary或@Qualifier注解来进行歧义消除,不能消除歧义的话,则会抛出异常。
@Autowired注解只有一个配置项required,用于指定注入的Bean是否是必需的,如果配置为true,则当容器无法找到匹配的Bean进行注入时,就会抛出异常,如果配置为false,则在此种情况下只会忽略而不会抛出异常。默认值为true。
4.11 @Primary注解
@Primary注解需要配合@Component(或其变体)注解或@Bean注解使用,表示其对应装配的Bean有更高的优先级。容器在处理@Autowired注解进行自动装配时,如果发现有多个匹配的Bean,则会有限选择带有@Primary注解标注的Bean来注入,通过这种方式来消除歧义。
@Primary注解没有可配置的选项。
4.12 @Qualifier注解
@Qualifier注解主要是配合@Autowired标签使用,在进行自动装配时,为@Autowired注解标注的属性、方法参数等指定合格的Bean。@Qualifier注解只有一个配置参数value,用于指定需要注入的Bean的名称,当容器处理@Autowired注解进行自动装配时,会根据类型和@Qualifier配置的Bean名称,在容器中查找与之匹配的Bean进行注入。使用@Qualifier注解可以消除根据类型匹配到多个Bean时出现的歧义,使容器能够按照被注入的Bean的需求注入合格的Bean。
4.13 @Value注解
@Value注解用于标注Bean的属性,将外部的值注入到Bean的属性中。外部值可以是指定的字面量,也可以是通过Spring EL表达式进行查找和计算得到的值。
@Value注解只有一个配置项value,指定需要注入到Bean属性的值。当value项的值使用“${…}”的方式配置时,表示通过占位符,注入配置属性中对应的配置项;当value的值使用“#{…}”的方式配置时,表示启用Spring EL表达式,通过表达式查找和计算,得到对应的值进行注入。
关于Spring EL表达式的内容,可以参见前面Spring EL表达式部分的内容。
4.14 @Profile注解
@Profile注解用于配合@Component注解(及其变体)和@Bean注解使用,让框架在不同的环境下,判断是否需要装配Bean。
@Profile注解只有一个配置参数value,用于指定需要装配Bean的环境名称。使用@Profile标注后,框架在处理@Component注解(及其变体)和@Bean注解时,会判断@Profile中配置的环境名称和应用当前的环境名称(由spring.profiles.active参数指定)是否匹配,如果匹配则装配Bean,否则不进行装配。
@Profile注解实质上是@Condition注解的变体,其本身标注了@Conditional注解,指定Condition接口的实现类ProfileCondition作为条件判断实现,这个实现类中会判断当前应用的环境名称和@Profile注解中配置的值是否一致,只有判断返回true的情况下,框架才会进行Bean的装配。