Spring的成功在于其提出的两个核心概念,一个是控制反转IoC,另一个则是面向切面编程AOP。IoC容器是Spring的核心,可以说Spring是一种基于IoC容器编程的框架,而Spring Boot是基于注解开发Spring IoC。
Ioc是一种生成或获取对象的技术,对于Java初学者更多时候所熟悉的是使用new关键字来创建对象,而在Spring中则不是,它是通过一些描述信息来创建对象, Spring早期常常使用XML来配置对象信息,而Spring Boot更建议使用注解的描述生成对象。
插叙:XML还是注解?
在Spring早期的1.x版本中,由于当时的JDK并不能支持注解,因此只能使用XML。XML是一种对象序列化的解决方案,并且被各种平台和语言使用,JDK曾发布过javax.xml.*类库。
JDK5加入了注解的新特性,这样注解就被广泛地使用起来。为了简化开发,在Spring 2.x之后的版本也引入了注解,不过只是少量的注解,如@Component、@Service等,但是功能还不够强大,此时Spring的开发还是以使用XML为主。到了Spring 3.0后,引入了更多的注解功能,于是在Spring中产生了这样一个很大的分歧,即是使用注解还是使用XML?对于XML的引入,有些人觉得过于繁复,而对于注解的使用,会使得注解分布得到处都是,难以控制,有时候还需要了解很多框架的内部实现才能准确使用注解开发所需的功能。这个时候大家形成了这样的一个不成文的共识,对于业务类使用注解,例如,对于MVC开发,控制器使用@Controller,业务层使用@Service,持久层使用@Repository;而对于一些公用的Bean,例如,对于数据库(如Redis)、第三方资源等则使用XML进行配置,直至今日这样的配置方式还在。Spring 3.x和Spring 4.x的版本注解功能越来越强大,对于XML的依赖越来越少,到了4.x的版本后甚至可以完全脱离XML,因此在Spring中使用注解开发占据了主流的地位。Pivotal团队在原有Spring的基础上主要通过注解的方式继续简化了Spring框架的开发,它们基于Spring框架开发了Spring Boot,所以Spring Boot并非是代替Spring框架,而是让Spring框架更加容易得到快速的使用。
IoC容器简介
Spring IoC容器是一个管理Bean的容器,在Spring的定义中,它要求所有的IoC容器都需要实
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 name) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
// 是否包含Bean
boolean containsBean(String name);
// Bean是否单例,默认的情况下,Bean都是以单例存在的
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
// Bean是否原型,若为true则每次获取该Bean时IoC容器都会创建新的对象并返回
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
// 是否类型匹配
boolean isTypeMatch(String name, ResolvableType typeToMatch)
throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class<?> typeToMatch)
throws NoSuchBeanDefinitionException;
// 获取Bean的类型
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
// 获取Bean的别名
String[] getAliases(String name);
}
getBean()是最常用的方法,用于从IoC容器中获取Bean,从源码可以看出可以按类型获取Bean,也可以按名称获取Bean。
Spring在BeanFactory的基础上,还设计了一个更为高级的子接口ApplicationContext。在Spring的体系中BeanFactory和ApplicationContext是最为重要的接口设计。在Spring Boot当中我们主要是通过注解来装配Bean到Spring IoC容器中,本文主要介绍一个基于注解的IoC容器,它就是AnnotationConfigApplicationContext,从名称就可以看出它是一个基于注解的IoC容器。之所以研究它,是因为Spring Boot装配和获取Bean的方法与它如出一辙。下面举个实例:
public class User {
private Long id;
private String username;
private String note;
/** getter setter **/
}
再定义一个配置类
@Configuration
public class AppConfig {
@Bean(name = "user")
public User initUser() {
User user = new User();
user.setId(1L);
user.setUserName("user_name_1");
user.setNote("note_1");
return user;
}
}
其中,@Configuration代表这是一个Java配置文件,Spring会根据它来生成IoC容器去装配Bean;@Bean代表将initUser方法返回的Bean装配到IoC容器中,而其属性name定义这个Bean的名称,如果没有配置它,则将方法名称“initUser”作为Bean的名称保存到Spring IoC容器中。
做好了这些,就可以使用AnnotationConfigApplicationContext来构建自己的IoC容器:
public class IoCTest {
private static Logger log = Logger.getLogger(IoCTest.class);
public static void main(String[] args) {
ApplicationContext ctx
= new AnnotationConfigApplicationContext(AppConfig.class);
User user = ctx.getBean(User.class);
log.info(user.getId());
}
}
代码中将Java配置文件AppConfig传递给AnnotationConfigApplicationContext的构造方法,这样它就能够读取配置了。然后将配置里面的Bean装配到IoC容器中,于是可以使用getBean方法获取对应的Bean。
装配Bean
Spring还允许我们进行扫描装配Bean到IoC容器中,扫描装配使用的注解是@Component和@ComponentScan。@Component是标明哪个类需要被扫描进入Spring IoC容器,而@ComponentScan则是标明采用何种策略去扫描装配Bean。举个例子:
package com.springboot.chapter3.config;
@Component("user")
public class User {
@Value("1")
private Long id;
@Value("user_name_1")
private String userName;
@Value("note_1")
private String note;
/**setter and getter **/
}
注意这里的User类在com.springboot.chapter3.config包下。这里的注解@Component表明这个类将被Spring IoC容器扫描装配,其中配置的“user”则是作为Bean的名称,当然你也可以不配置这个字符串,那么IoC容器就会把类名第一个字母作为小写,其他不变作为Bean名称放入到IoC容器中;注解@Value则是指定具体的值,使得Spring IoC给予对应的属性注入对应的值。为了让Spring IoC容器装配这个类,需要改造类AppConfig:
package com.springboot.chapter3.config;
/** import ... **/
@Configuration
@ComponentScan
public class AppConfig { }
这里加入了@ComponentScan,意味着它会进行扫描,但是它只会扫描类AppConfig所在的当前包和其子包,之前把User.java移到包com.springboot.chapter3.config就是这个原因。这样就可以删掉之前使用@Bean标注的创建对象方法,相对方便。为了更加合理,@ComponentScan还允许我们自定义扫描的包,可以通过配置项basePackages定义扫描的包名,在没有定义的情况下,它只会扫描当前包和其子包下的路径;还可以通过basePackageClasses定义扫描的类;其中还有includeFilters和excludeFilters,includeFilters是定义满足过滤器(Filter)条件的Bean才去扫描,excludeFilters则是排除过滤器条件的Bean,它们都需要通过一个注解@Filter去定义。
事实上,在Spring Boot实例中常看到的注解@SpringBootApplication也注入了@ComponentScan。
现实的Java的应用往往需要引入许多来自第三方的包,并且很有可能希望把第三方包的类对象也放入到Spring IoC容器中,这时@Bean注解就可以发挥作用了。如下实例,我们利用@Bean生成一个数据库的数据源,Spring就会把方法返回的对象用名称“dataSource”保存在IoC容器中。
@Bean(name = "dataSource")
public DataSource getDataSource() {
Properties props = new Properties();
props.setProperty("driver", "com.mysql.jdbc.Driver");
props.setProperty("url", "jdbc:mysql://localhost:3306/chapter3");
props.setProperty("username", "root");
props.setProperty("password", "123456");
DataSource dataSource = null;
try {
dataSource = BasicDataSourceFactory.createDataSource(props);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
依赖注入
Bean之间的依赖,在Spring IoC的概念中,我们称为依赖注入(Dependency Injection,DI)。@Autowired是我们在Spring中最常用的注解之一,它会根据属性的类型(by type)找到对应的Bean进行注入。我们回顾IoC容器的顶级接口BeanFactory,就可以知道IoC容器是通过getBean方法获取对应Bean的,而getBean又支持根据类型(by type)或者根据名称(by name)。首先它会根据类型找到对应的Bean,如果对应类型的Bean不是唯一的,那么它会根据其属性名称和Bean的名称进行匹配。如果匹配得上,就会使用该Bean;如果还无法匹配,就会抛出异常。还要注意的是@Autowired是一个默认必须找到对应Bean的注解,如果不能确定其标注属性一定会存在并且允许这个被标注的属性为null,那么你可以配置@Autowired属性required为false。
@Primary是一个修改优先权的注解,可以搭配@Bean或@Component使用,含义是告诉Spring IoC容器,当发现有多个同样类型的Bean时,请优先使用带有该注解的Bean。但如果存在多个同类型的Bean,而且有多个Bean都使用了@Primary,那么IoC容器还是无法区分采用哪个Bean。对于此情况,可以使用@Qualifier,它的配置项value需要一个字符串去定义,它将与@Autowired组合在一起,通过类型和名称一起找到Bean。此时你是否想起了BeanFactory接口中的这个方法呢?
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
通过上述方法就可以通过类型和名称找到对象。
上述例子中,我们都基于不带参数的构造方法下实现依赖注入。但事实上有些类只有带有参数的构造方法,于是上述的方法都不能再使用了。我们可以使用@Autowired注解对构造方法的参数进行注入。我们可以取消@Autowired对属性和方法的标注,并在构造方法的参数前加上@Autowired注解,使得它能够注入进来。
生命周期
有时我们也需要自定义初始化或者销毁Bean的过程,以满足一些要求。因此有必要了解Spring IoC初始化和销毁Bean的过程,它大致分为Bean定义、Bean的初始化、Bean的生存期和Bean的销毁4个部分。
Spring通过我们的配置,如@ComponentScan定义的扫描路径去找到带有@Component的类, 这是一个资源定位的过程。一旦找到了资源,就开始解析并且将定义的信息保存起来。注意,此时还没有初始化Bean,也就没有Bean的实例,有的仅仅是Bean的定义。然后就会把Bean定义发布到Spring IoC容器中。此时,IoC容器也只有Bean的定义,还是没有Bean的实例生成。默认情况下,Spring会继续去完成Bean的实例化和依赖注入,这样从IoC容器中就可以得到一个依赖注入完成的Bean。但是有时我们倒希望让那些Bean只是将定义发布到IoC容器而不做实例化和依赖注入,当我们取出来的时候才做初始化和依赖注入等操作。
ComponentScan中还有一个配置项lazyInit,且默认值为false,也就是默认不进行延迟初始化,因此在默认的情况下Spring会对Bean进行实例化和依赖注入对应的属性值。
![](https://img-blog.csdnimg.cn/direct/def046196e244d49ba784f5a65e7bdaf.png)
生命周期接口
与Bean的生命周期相关的接口有BeanNameAware、BeanFactoryAware、ApplicationContextAware、 InitializingBean,、DisposableBean。通过注解@PostConstruct 定义了初始化方法,通过注解@PreDestroy 定义了销毁方法。
后置Bean初始化器接口
BeanPostProcessor接口使用示例如下,注意,这个 Bean 后置处理器将对所有的 Bean 有效。
@Component
public class BeanPostProcessorExample implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
System.out.println("BeanPostProcessor 调用"
+ "postProcessBeforeInitialization 方法,参数【"
+ bean.getClass().getSimpleName() + "】【" + beanName + "】 ");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
System.out.println("BeanPostProcessor 调用"
+ "postProcessAfterInitialization 方法,参数【"
+ bean.getClass().getSimpleName() + "】【" + beanName + "】 ");
return bean;
}
}
application.properties文件
在 Spring Boot 中使用属性文件,可以采用其默认为我们准备的 application.properties,也可以使用自定义的配置文件。在 Spring Boot 中,我们先在 Maven 配置文件中加载依赖,如下代码清单所示,这样 Spring Boot 将创建读取属性文件的上下文。有了依赖,就可以直接使用 application.properties 文件为你工作了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/chapter3
database.username=root
database.password=123456
我们可以通过@Value 注解,使用${......}这样的占位符读取配置在属性文件的内容。
@Component
public class DataBaseProperties {
@Value("${database.driverName}")
private String driverName = null;
@Value("${database.url}")
private String url = null;
private String username = null;
private String password = null;
@Value("${database.username}")
public void setUsername(String username) {
System.out.println(username);
this.username = username;
}
/**** getters/setters ****/
}
有时候我们也可以使用注解@ConfigurationProperties,通过它使得配置上 有所减少,如下
@Component
@ConfigurationProperties("database")
public class DataBaseProperties {
private String driverName = null;
private String url = null;
private String username = null;
private String password = null;
/******** getters/setters ********/
}
注解@ConfigurationProperties 中配置的字符串 database,将与 POJO 的属性名称组成属性
的全限定名去配置文件里查找,这样就能将对应的属性读入到 POJO 当中。
有时候我们会觉得如果把所有的内容都配置到 application.properties,这个文件将有很多内 容。我们可以选择使用新的属性文件。例如,数据库的属性可以配置在 jdbc.properties 中,于是先把之前代码清单中给出的配置从 application.properties 中迁移到 jdbc.properties 中,然后使用@PropertySource 去定义对应的属性文件,把它加载到 Spring 的上下文中。
@SpringBootApplication
@ComponentScan(basePackages = {"com.springboot.chapter3"})
@PropertySource(value={"classpath:jdbc.properties"}, ignoreResourceNotFound=true)
public class Chapter3Application {
public static void main(String[] args) {
SpringApplication.run(Chapter3Application.class, args);
}
}
value 可配置多个配置文件。使用 classpath 前缀意味着去类文件路径下找到属性文件; ignoreResourceNotFound 则是是否忽略配置文件找不到的问题,默认值为 false,也就是没有找到属性文件就会报错;这里配置为 true,也就是找不到就忽略掉,不会报错。
条件装配
在数据库连接池的配置中漏掉一 些配置会造成数据源不能连接上。在这样的情况下,IoC 容器如果还进行数据源的装配,则系统将会抛出异常,导致应用无法继续。这时倒是希望 IoC 容器不去装配数据源。例子如下,注意DatabaseConditional类必须实现Condition接口,实现matches方法,通常在matches中判断配置信息是否齐全。
@Bean(name = "dataSource", destroyMethod = "close")
@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 props = new Properties();
props.setProperty("driver", driver);
props.setProperty("url", url);
props.setProperty("username", username);
props.setProperty("password", password);
DataSource dataSource = null;
try {
dataSource = BasicDataSourceFactory.createDataSource(props);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
Bean的作用域
作用域类型 | 使用范围 | 作用域描述 |
---|---|---|
singleton | 所有spring的应用 | 默认,IoC容器中只存在单例 |
prototype | 所有spring的应用 | 每次从IoC容器中取出的都是新创建的 |
session | Spring Web应用 | HTTP会话 |
application | Spring Web应用 | Web工程生命周期 |
request | Spring Web应用 | 单次请求 |
globalSession | Spring Web应用 | 实践中基本不使用 |
可以通过@Scope注解对Bean的作用域进行修改。对于 application 作用域,完全可以使用单例来替代。如果是在 Spring MVC 环境中,可以使用 WebApplicationContext 去定义其他作用域,如请求(SCOPE_REQUEST)、会话(SCOPE_SESSION)和应用(SCOPE_ APPLICATION)。