目录
IOC(控制反转)是Spring的核心,可以说Spring是一种基于IOC容器编程的框架。因为Spring Boot是基于注解的开发Spring IOC,所以我们使用全注解的方式讲述Spring IOC技术。
1.IOC容器简介
SpringIOC容器是一个管理Bean的容器,在Spring的定义中,它要求所有的IOC容器都需要实现接口BeanFactory,它是一个顶级的容器接口。BeanFactory的实现代码如下;
package org.springframework.beans.factory;
import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
public interface BeanFactory {
//前缀
String FACTORY_BEAN_PREFIX = "&";
//多个getBean方法
Object getBean(String var1) throws BeansException;
<T> T getBean(String var1, Class<T> var2) throws BeansException;
<T> T getBean(Class<T> var1) throws BeansException;
Object getBean(String var1, Object... var2) throws BeansException;
<T> T getBean(Class<T> var1, Object... var2) throws BeansException;
//是否包含Bean
boolean containsBean(String var1);
//是否是单例
boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;
//是否原型
boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;
//是否类型匹配
boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;
//获取Bean类型
Class<?> getType(String var1) throws NoSuchBeanDefinitionException;
//获取Bean别名
String[] getAliases(String var1);
}
Spring IOC容器中,默认的情况下,Bean都是以单例存在的,也就是说getBean方法返回的都是同一个对象。isSingleton方法则判断Bean是否在Spring IOC中为单例。与isSingleton相反的是isPrototype方法,如果它返回的是true,那么我们使用getBean方法获取Bean的时候,Spring IOC容器就会创建一个新的Bean返回给调用者。
由于BeanFactory的功能还不够强大,因此Spring在BeanFactory的基础上,设计了更为高级的接口ApplicationContext。它是BeanFactory的子接口之一,在Spring的体系中BeanFactory和ApplicationContext是最重要的两个接口。在Spring中,我们使用的大部分的Spring IOC容器都是ApplicationContext接口的实现类,它们的关系如下所示:
ApplicationContext接口通过继承上级接口,进而继承BeanFactory接口,但是在BeanFactory的基础上,扩展了消息国际化接口、环境可配置接口、应用事件发布接口和资源模式解析接口,所以它的功能会更为强大。
为了贴近Spring Boot的需要,这里主要介绍一个基于注解的IOC容器:AnnotationConfigApplicationContext,它是一个基于注解的IOC容器。Spring Boot装配和获取Bean的方法和该容器如出一辙。首先看如下简单的实例:
定义一个pojo:
package com.martin.config.chapter3.pojo;
import lombok.Data;
/**
* @author: martin
* @date: 2019/10/27 17:33
* @description:
*/
@Data
public class User {
private Long id;
private String userName;
private String note;
}
定义Java配置文件:
package com.martin.config.chapter3.config;
import com.martin.config.chapter3.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author: martin
* @date: 2019/10/27 17:39
* @description:
*/
@Configuration
public class AppConfig {
@Bean("user")
public User initUser() {
User user = new User();
user.setId(1L);
user.setUserName("martin");
user.setNote("note_1");
return user;
}
}
这里@Configuration代表这是一个Java配置文件,Spring的容器会根据它来生成IOC容器去装配Bean;@Bean代表将initUser方法返回的POJO装配到IOC容器中,而其属性name定义了这个Bean的名称,如果没有配置它,则将方法名称“initUser”作为Bean的名称保存到Spring IOC容器中。最后,我们使用AnnotationConfigApplicationContext来构建自己的IOC容器,代码清单如下:
package com.martin.config.chapter3;
import com.martin.config.chapter3.config.AppConfig;
import com.martin.config.chapter3.pojo.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* @author: martin
* @date: 2019/10/27 18:57
* @description:
*/
public class IocTest {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
User user = ctx.getBean(User.class);
System.out.println(user.getId());
}
}
2.装配Bean
2.1 通过扫描装配Bean
除了使用注解@Bean注入Spring IOC容器,我们还可以使用扫描装配Bean到IOC容器中,对于扫描装配而言使用的注解是@Component和@ComponentScan。@Component是标明哪个类被扫描进入Spring IOC容器,而@ComponentScan则是标明采用何种策略去扫描装配Bean。User使用@Component的实现代码如下:
package com.martin.config.chapter3.pojo;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/10/27 17:33
* @description:
*/
@Data
@Component("user")
public class User {
@Value("1")
private Long id;
@Value("martin")
private String userName;
@Value("note")
private String note;
}
这里的注解@Component表明这个类将被Spring IOC容器扫描装配,其中配置的“user”则是作为Bean的名称。如果不配置这个字符串,IOC容器会把类名的第一个字母作为小写,其他不变作为Bean名称放入到IOC容器中;注解@Value则指定具体的值,使得Spring IOC给与对应的属性注入对应的值。
为了让IOC容器能装配User这个类,需要改造AppConfig,代码如下:
package com.martin.config.chapter3.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author: martin
* @date: 2019/10/27 17:39
* @description:
*/
@Configuration
@ComponentScan
public class AppConfig {
}
这里加入了@ComponentScan,意味着它会进行自动扫描,但是它只会扫描类AppConfig所在的当前包和其子包,User类不属于AppConfig的包或子包中的类,因此无法扫描到。如果想被扫描到,可以自定义扫描的包,可以通过basePackages定义扫描的包名,还可以通过basePackageClasses定义扫描的类。代码如下:
@ComponentScan("com.martin.config.chapter3.pojo.*")
@ComponentScan(basePackages = "com.martin.config.chapter3.pojo.*")
@ComponentScan(basePackageClasses = User.class)
按照以上的配置策略,User类会被扫描到Spring IOC容器中。
2.2 自定义第三方Bean
现实的Java的应用往往需要引入许多来自第三方的包,并且希望把第三方包的类对象也放入到Spring IOC容器中,这时@Bean注解就可以发挥作用了。例如我们定义DHCP数据源,POM文件如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
这样DHCP和数据库驱动就被加入到了项目中,接着将使用它提供的机制来生成数据源。代码如下:
package com.martin.config.chapter3.config;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Properties;
/**
* @author: martin
* @date: 2019/10/27 17:39
* @description:
*/
@Configuration
public class AppConfig {
@Bean("dataSource")
public DataSource getDataSource() {
Properties properties = new Properties();
properties.setProperty("driver", "com.mysql.jdbc.Driver");
properties.setProperty("url", "jdbc:mysql://localhost:3306/chapter3");
properties.setProperty("username", "root");
properties.setProperty("password", "123456");
try {
return BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
这里通过@Bean定义了其配置项name为dataSource,IOC容器就会把它作为对象名保存起来。
3.依赖注入
不同Bean之间的依赖被称为依赖注入,依赖注入主要使用@Autowired注解。例如我们注入一个user属性:
@Autowired
private User user;
@Autowired注解也可以用到方法上:
@Autowired
public void setUser(User user) {
this.user = user;
}
@Autowired是一个默认必须找到对应Bean的注解,如果不能确定其标注属性一定会存在并且允许这个被标注的属性为null,那么可以配置@Autowired属性required为false。
@Autowired(required = false)
private User user;
如果User的实现类有多个,比如管理员admin,普通用户general ,此时单纯使用@Autowired注解,IOC容器时无法区分采用哪个Bean实例注入。我们可以使用@Quelifier注解,与@Autowired组合在一起,通过类型和名称一起找到Bean。
@Autowired
@Qualifier("admin")
private User user;
Spring IOC将会以类型和名称一起去寻找对应的Bean进行注入。
4.生命周期
Bean的声明周期过程,大致分为Bean定义、Bean的初始化、Bean的生存期和Bean的销毁4个部分,其中Bean定义过程大致如下:
- Spring通过配置,如@ComponentScan定义的扫描路径去找到带有@Component的类,这个过程就是一个资源定位的过程
- 一旦找到资源,那么就开始解析,并且将定义的信息保存起来。注意,此时还没有初始化Bean,也没有Bean的实例,仅仅有Bean的定义
- 把Bean定义发布到Spring IOC容器中。
完成了这3步只是一个资源定位并将Bean的定义发布到IOC容器的过程,还没有Bean实例的生成,更没有完成依赖注入。在默认情况下,接下来的步骤是完成Bean的实例化和依赖注入,这样就能从IOC容器中获取到一个Bean实例。如果我们设置lazyInit(懒加载)的值为true,那么Spring并不会在发布Bean定义后马上为我们完成实例化和依赖注入,而是要等到真正使用到的时候才开始实例化和依赖注入。
Spring在完成依赖注入之后,还提供了一系列的接口和配置来完成Bean的初始化过程,整个流程如下:
Spring IOC容器最低的要求是实现BeanFactory接口,而不是实现ApplicationContext接口。对于那些没有实现ApplicationContext接口的容器,在生命周期对应的ApplicationContextAware定义的方法也不会调用,只有实现了ApplicationContext接口的容器,才会在生命周期调用ApplicationContextAware所定义的setApplicationContext方法。为了测试生命周期我们定义一个BussinessPerson类,代码如下:
package com.martin.config.chapter3.service;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* @author: martin
* @date: 2019/10/28 22:09
* @description:
*/
@Component
public class BussinessPerson implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("调用BeanFactoryAware的setBeanFactory");
}
@Override
public void setBeanName(String name) {
System.out.println("调用BeanNameAware的setBeanName方法:" + name);
}
@Override
public void destroy() throws Exception {
System.out.println("调用DisposableBean的destroy");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("调用InitializingBean的afterPropertiesSet");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("调用ApplicationContextAware的setApplicationContext");
}
@PostConstruct
public void init() {
System.out.println("调用@PostConstruct标识的方法");
}
@PreDestroy
public void preDestroy() {
System.out.println("调用@PreDestroy标识的方法");
}
}
这样,BussinessPerson 这个Bean就实现了生命周期中单个Bean可以实现的所有接口。为了测试Bean的后置处理器,我们创建一个类BeanPostProcessorExample,该后置处理器将对所有的Bean都有效,代码如下:
package com.martin.config.chapter3.service;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/10/28 22:31
* @description:
*/
@Component
public class BeanPostProcessorTest implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanPostProcessor调用before" + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanPostProcessor调用After" + beanName);
return bean;
}
}
运行Spring Boot应用程序,输出结果如下:
调用BeanNameAware的setBeanName方法:bussinessPerson
调用BeanFactoryAware的setBeanFactory
调用ApplicationContextAware的setApplicationContext
BeanPostProcessor调用beforebussinessPerson
调用@PostConstruct标识的方法
调用InitializingBean的afterPropertiesSet
BeanPostProcessor调用AfterbussinessPerson
BeanPostProcessor调用beforedataSource
BeanPostProcessor调用AfterdataSource
对于Bean后置处理器(BeanPostProcessor)而言,它对所有的Bean都起作用,而其他的接口则是对于单个Bean起作用。有时候Bean的定义可能使用的是第三方的类,此时可以使用注解@Bean来配置自定义初始化和销毁方法,代码如下:
@Bean(initMethod='init',destroyMethod='destroy')
5.使用属性文件
在Spring Boot中使用属性文件,可以采用默认的application.properties,也可以使用自定义的配置文件。在Spring Boot中,我们在Maven中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
有了该依赖,就可以直接使用application.properties文件为我们工作了。配置文件如下:
database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/test
database.username=root
database.password=123456
我们使用Spring表达式的方式获取配置的属性值:
package com.martin.config.chapter3.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/11/2 17:23
* @description:
*/
@Component
public class DataBaseProperties {
@Value("${database.driverName}")
private String driverName;
@Value("${database.url}")
private String url;
private String password;
private String userName;
public String getDriverName() {
return driverName;
}
public void setDriverName(String driverName) {
this.driverName = driverName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getPassword() {
return password;
}
@Value("${database.password}")
public void setPassword(String password) {
this.password = password;
}
public String getUserName() {
return userName;
}
@Value("${database.username}")
public void setUserName(String userName) {
this.userName = userName;
}
}
通过Spring表达式@Value注解,读取配置在属性文件的内容。@Value注解既可以加载属性,也可以加在方法上。
我们也可以使用注解@ConfigurationProperties来配置属性,代码如下:
package com.martin.config.chapter3.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/11/2 17:23
* @description:
*/
@Component
@ConfigurationProperties("database")
public class DataBaseProperties {
private String driverName;
private String url;
private String password;
private String userName;
public String getDriverName() {
return driverName;
}
public void setDriverName(String driverName) {
this.driverName = driverName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
这里在@ConfigurationProperties中配置的字符串database,将与POJO的属性名称组成属性属性的全限定名去配置文件里查找,这样就能将对应的属性读取到POJO当中。
有时候我们会觉得如果把所有的属性配置到放置到application.properties中,这个文件将会有很多属性内容。为了更好的配置,我们将数据库的属性配置到jdbc.properties中,然后使用@PropertySource去定义对应的属性文件,把它加载到Spring的上下文中。代码如下所示:
package com.martin.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;
/**
* 配置启动类
*
* @author martin
* @create 2019-01-03 下午 11:20
**/
@SpringBootApplication
@PropertySource(value = {"classpath:jdbc.properties"}, ignoreResourceNotFound = true)
public class SpringBootConfigApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootConfigApplication.class, args);
}
}
value可以配置多个配置文件,使用classpath前缀,意味着去类文件路径下找到属性文件;ignoreResourceNotFound则是是否忽略配置文件找不到的问题,默认值是false,也就是找不到属性文件就会报错。
6.条件装配Bean
有时候我们希望IOC容器在某些条件满足下才去装配Bean,例如我们要求在数据库的配置中,驱动名、url、账号和密码都存在的情况下才去连接数据库。此时我们需要使用@Conditional注解和实现Condition接口的类。代码如下:
package com.martin.config.chapter3.config;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Properties;
/**
* @author: martin
* @date: 2019/10/27 17:39
* @description:
*/
@Configuration
public class AppConfig {
@Bean("dataSource")
@Conditional(DatabaseConditional.class)
public DataSource getDataSource(@Value("${database.driverName") String driver,
@Value("${database.url") String url,
@Value("${database.username") String username,
@Value("${database.password") String password) {
Properties properties = new Properties();
properties.setProperty("driver", driver);
properties.setProperty("url", url);
properties.setProperty("username", username);
properties.setProperty("password", password);
try {
return BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
加入了@Conditionnal注解,并且配置了类DatabaseConditional,那么这个类就必须实现Condition接口。代码如下:
package com.martin.config.chapter3.config;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* @author: martin
* @date: 2019/11/2 20:08
* @description:
*/
public class DatabaseConditional implements Condition {
/**
* 数据库装配条件
*
* @param context 条件上下文
* @param metadata 注释类型的元数据
* @return true装配Bean,否则不装配
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
//判断属性文件是否存在对应的数据库配置
return environment.containsProperty("database.driverName")
&& environment.containsProperty("database.url")
&& environment.containsProperty("database.username")
&& environment.containsProperty("database.password");
}
}
maches方法读取上下文环境,判断是否已经配置了对应的数据库信息。当都配置好了以后返回true,Spring会装配数据库连接池的Bean,否则不装配。
7.Bean的作用域
IOC容器最顶级接口BeanFactory中,可以看到isSingleton和isPrototype两个方法。其中,如果isSingleton方法如果返回true,则Bean在IOC容器中以单例存在,这也是Spring IOC容器的默认值。如果isPrototype方法返回true,则当我们每次获取Bean的时候,IOC容器都会创建一个新的Bean。除了这些,Bean还存在其他类型的作用域:
对于application作用域,完全可以使用单例来代替。
作用域的定义使用@Scope注解,实例代码如下:
package com.martin.config.chapter3.service;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
/**
* @author: martin
* @date: 2019/11/2 22:25
* @description:
*/
@Component
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class ScopeBean {
}
8.@Profile
在企业开发的过程中,项目往往需要面临开发环境,测试环境,愈发环境和生产环境的切换,而每一套环境的上下文是不一样的。例如,它们会有各自的数据库资源,这样就需要我们再不同的数据库之前进行切换。为了方便,Spring提供了Profile机制,使得我们可以方便地实现各个环境之间的切换。
在Spring中存在两个配置参数可以提供给我们修改启动Profile机制,一个是spring.profiles.active,另外一个是spring.profiles.default。在两个属性参数都没有配置的情况下,Spring将不会启动Profile机制。Spring先判断是否存在spring.profiles.active配置之后,再去查找spring.profiles.default配置,所以spring.profiles.active优先级大于spring.profiles.default。
在Java项目启动时,我们配置如下就能够启动Profile机制:
JAVA_OPTS=“-Dspring.profiles.active=dev”
在Spring Boot的规则中,假设把选项-Dspring.profiles.active配置的值记为{profile},它就会用application-{profile}.properties文件去代替原来默认的application.properties文件,然后启动Spring Boot的程序。
9.引入XML配置的Bean
尽管Spring Boot建议使用注解和扫描配置Bean,但是它并不拒绝使用XML配置Bean。如果我们想在Spring Boot中使用XML对Bean进行配置,可以使用注解@ImportResource,通过它可以引入对应的XML文件,用以加载Bean。实例代码如下:
Configuration
@ImportResource(value = {"classpath:spring-job.xml"})
public class AppConfig {
}
这样就可以引入对应的XML,从而将XML定义的Bean装配到IOC容器中。
10.Spring EL表达式
在前面的代码中,我们是在没有任何运算规则的情况下装配Bean的。为了更加灵活,Spring EL表达式为我们提供了强大的运算规则来更好的装配Bean。
EL表达式最常用的是读取配置属性文件中的值,例如:
@Value("${database.driverName}")
除此之外,它还能够调用方法,例如,记录Bean的初始化时间:
@Value("#{T(System).currentTimeMillis()}")
private Long time;
这里采用#{.......}代表启用Spring表达式,它将具有运算的功能;T(......)代表的是引入类;System是java.lang.*包的类,这是java 默认加载的包,可以不用写全限定名。如果是其他的包,需要写出全限定名才能引用类。此外EL表达式支持多种赋值方式,代码如下:
package com.martin.config.chapter3.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/11/2 23:06
* @description:
*/
@Component
public class SpringEl {
@Value("${database.driverName}")
private String driver;
@Value("#{T(System).currentTimeMillis()}")
private Long time;
/**
* 使用字符串直接赋值
*/
@Value("#{'使用Spring EL赋值字符串'}")
private String str;
/**
* 科学计数法赋值
*/
@Value("#{9.3E3}")
private Double d;
/**
* 赋值浮点数
*/
@Value("#{3.14}")
private float pi;
/**
* 使用其他Bean的属性来赋值
*/
@Value("#{beanName.str}")
private String otherBeanProp;
/**
* 使用其他Bean的属性来赋值,转换为大写
*/
@Value("#{beanName.str?.toUpperCase()}")
private String otherBeanPropUpperCase;
/**
* 数学运算
*/
@Value("#{1+2}")
private int run;
/**
* 浮点数比较运算
*/
@Value("#{beanName.pi == 3.14f}")
private boolean piFlag;
}