你只管使用,一切交给IOC。
通过组件注册,IOC就自动帮助我们管理bean。
创建项目,引入依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
@Bean注册组件
创建一个实例类User
:
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class User {
private Long id;
private String name;
}
接着创建一个配置类,在配置类中通过@Bean
注解注册User
类:
@Configuration
public class Config {
@Bean
public User user() {
return new User();
}
}
在SpringBoot入口类中编写如下代码:
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
User user = applicationContext.getBean(User.class);
System.out.println(user);
}
}
运行启动类,控制台输出如下:
User(id=1, name=KHighness)
@ComponentSacn扫描包
在这个SpringBoot启动类所在包下创建其他类:UserDao、UserService、UserController,并且标注上响应注解,给User
类标注@Component
注解。
并修改配置类:
@Configuration
@ComponentScan("top.parak")
public class WebConfig {
}
早在启动类中获取所有的bean:
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
String[] names = applicationContext.getBeanDefinitionNames();
Arrays.stream(names).forEach(System.out::println);
}
}
运行启动类,观察控制台,有如下输出:
KHighnessApplication
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
webConfig
userController
userDao
user
userService
@Scope组件作用域
默认情况下,在Spring的IOC容器中每个组件都是单例的,即无论在任何地方注入多少次,这些对象都是同一个。
将配置类修改回去:
@Configuration
public class WebConfig {
@Bean
public User user() {
return new User(1L, "KHighness");
}
}
修改SpringBoot启动类如下:
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
Object user1 = applicationContext.getBean("user");
Object user2 = applicationContext.getBean("user");
System.out.println(user1 == user2);
}
}
运行启动类,控制台输出如下:
true
可以通过@Scope
修改作用域,具体如下:
类型 | 描述 |
---|---|
singleton | 单实例(默认),在Spring IOC容器启动的时候会调用方法创建对象然后纳入到IOC容器中,以后每次获取都是直接从IOC容器中获取 |
prototype | 多实例,IOC容器启动的时候并不会去创建对象,而是在每次获取的时候才会去调用方法创建对象 |
request | 一个请求对应一个实例 |
session | 同一个session对应一个实例 |
@Lazy懒加载
懒加载是针对单例模式而言的,容器启动的时候会调用方法创建对象然后纳入到IOC容器中。
在User注册的地方打印一句话以观察:
@Bean
public User user() {
System.out.println("向IOC容器中注册user");
return new User(1L, "KHighness");
}
在IOC容器启动后加入一句话以观察:
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
System.out.println("IOC容器创建完毕");
运行启动类,观察控制台,有如下输出:
向IOC容器中注册user
2021-06-28 16:43:13.064 INFO 49772 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-06-28 16:43:13.165 INFO 49772 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-06-28 16:43:13.168 INFO 49772 --- [ main] top.parak.KHighnessApplication : Started KHighnessApplication in 1.081 seconds (JVM running for 1.667)
IOC容器创建完毕
可以看到,在IOC容器创建完毕之前,组件已经注册到容器中。
将User修改为懒加载注册:
@Bean
@Lazy
public User user() {
System.out.println("向IOC容器中注册user");
return new User(1L, "KHighness");
}
将启动类修改一下,获取user:
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
System.out.println("IOC容器创建完毕");
Object user = applicationContext.getBean("user");
System.out.println(user);
}
}
运行启动类,观察控制台,有如下输出:
IOC容器创建完毕
向IOC容器中注册user
User(id=1, name=KHighness)
从输出看到,创建时并没有注册user,获取时才去注册,可以证明懒加载成功。
条件注册
@Conditional
使用@Conditional
注解我们指定组件注册的条件,即满足特定条件才将组件纳入到IOC容器中。
在使用该注解之前,先创建一个类,实现Condition
接口:
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return false;
}
}
该接口包含一个matches
方法,包含两个入参:
ConditionContext
:上下文信息;AnnotatedTypeMetadata
:注解信息。
简单完善一下实现类:
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
String osName = conditionContext.getEnvironment().getProperty("os.name");
return osName != null && osName.contains("windows");
}
}
将这个条件添加到注册User的地方:
@Bean
@Conditional(MyCondition.class)
public User user() {
System.out.println("向IOC容器中注册user");
return new User(1L, "KHighness");
}
在Windows系统下,User会被成功注册,其他操作系统下,这个组件则不会被注册到IOC容器中。
@Profile
@Profile
可以根据不同的环境变量来注册不同的组件。
新建一个接口CalculateService
,用于求和:
public interface CalculateService {
Integer sum(Integer... value);
}
接着添加两个实现类Java7CalculateServiceImpl
和Java8CalculateServiceImpl
:
@Service
@Profile("java7")
public class Java7CalculateServiceImpl implements CalculateService {
@Override
public Integer sum(Integer... value) {
System.out.println("Java7环境下执行");
int res = 0;
for (int val : value)
res += val;
return res;
}
}
@Service
@Profile("java8")
public class Java8CalculateServiceImpl implements CalculateService {
@Override
public Integer sum(Integer... value) {
System.out.println("Java8环境下执行");
return Arrays.stream(value).reduce(0, Integer::sum);
}
}
通过@Profile
注解我们实现了:当环境变量包含java7
的时候,Java7CalculateServiceImpl
将会被注册到IOC容器中;当环境变量包含java8
的时候,Java8CalculateServiceImpl
将会被注册到IOC容器中。
修改启动类如下:
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.profiles("java8")
.run(args);
CalculateService service = applicationContext.getBean(CalculateService.class);
System.out.println("求和结果:" + service.sum(1, 2, 3, 4, 5));
}
}
运行启动类,观察控制台,有如下输出:
Java8环境下执行
求和结果:15
同理,如果设置了环境变量java7
的时候,将会输出Java7环境下执行
。
导入组件
@Import
上面学习了使用@Bean
和@@ComponentScan
扫描实现组件注册,除此之外,我们还可以使用@Import
来快速地往IOC容器中添加组件。
创建一个新的类Hello
:
public class Hello {
}
然后在配置类中导入这个组件:
@Configuration
@Import({Hello.class})
public class WebConfig {...
修改启动类,查看所有组件的名称:
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
String[] names = applicationContext.getBeanDefinitionNames();
Arrays.stream(names).forEach(System.out::println);
}
}
运行启动类,观察控制台,有如下输出:
top.parak.entity.Hello
可以看到,通过@Import
我们可以快速向容器中添加组件,默认名称为全类名。
ImportSelector
通过@Import
可以实现组件导入,如果需要一次性导入较多组件,我们可以使用@ImportSelector
来实现。
查看ImportSelector
源码:
public interface ImportSelector {
/**
* Select and return the names of which class(es) should be imported based on
* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
@ImportSelector
是一个接口,包含一个selectImports
方法,方法返回的类的全类名数组(即需要导入到IOC容器中组件的全类名数组),包含一个@AnnotationMetadata
类型入参,通过这个参数我们可以获取到使用@ImportSelector
的类的全部注解信息。
新建三个类,分别为Orange
、Banana
和Pineapple
。
新建一个ImportSelector
实现类MyImportSelector
:
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {
"top.parak.entity.Orange",
"top.parak.entity.Banana",
"top.parak.entity.Pineapple"
};
}
}
接着通过@Import
注解将MyImportSelector
把丧组件快速导入到容器中:
@Import(MyImportSelector.class)
public class WebConfig {...
运行启动类,观察控制台,发现有如下输出:
top.parak.entity.Orange
top.parak.entity.Banana
top.parak.entity.Pineapple
说明三个组件已经批量导入。
ImportBeanDefinitionRegistor
除了上述两种向IOC容器中导入组件的方法外,我们还可以使用ImportBeanDefinitionRegistrar
来手动往IOC容器中导入组件。
查看ImportBeanDefinitionRegistrar
源码:
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
ImportBeanDefinitionRegistrar
是一个接口,包含一个registerBeanDefinitions
方法,该方法包含两个入参:
AnnotationMetadata
:可以通过它获取类的注解信息;BeanDefinitionRegistry
:Bean定义注册器,包含了一些和Bean有关的方法: ```java public interface BeanDefinitionRegistry extends AliasRegistry {void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException;
void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
boolean containsBeanDefinition(String beanName);
String[] getBeanDefinitionNames();
int getBeanDefinitionCount();
boolean isBeanNameInUse(String beanName);
}
这里借助`BeanDefinitionRegistry`的`registerBeanDefinition`方法往IOC容器中注册Bean。该方法包含两个入参,第一个参数为需要注册的Bean名称,第二个参数为Bean的定义信息,它是一个接口,我们可以用其实现类`RootBeanDefinition`来完成:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/493248/1624932571373-b470a8b7-6d81-43db-ab8d-3519fc966015.png#clientId=u3270af3a-3b6d-4&from=paste&height=262&id=u9e2755e6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=262&originWidth=952&originalType=binary&ratio=1&size=70490&status=done&style=none&taskId=u2fba5581-d818-4570-a8a6-69c5226dd34&width=952)<br />为了演示`ImportBeanDefinitionRegistrar`的使用,新增一个类`Blueberry`。<br />创建一个`ImportBeanDefinitionRegistrar`实现类`MyImportBeanDefinitionRegistrar`:
```java
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
final String beanName = "blueberry";
boolean contain = registry.containsBeanDefinition(beanName);
if (!contain) {
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Blueberry.class);
registry.registerBeanDefinition(beanName, rootBeanDefinition);
}
}
}
然后在配置类中导入:
@Configuration
@Import({MyImportBeanDefinitionRegistrar.class})
public class WebConfig {...
运行启动类,观察控制台,有如下输出:
blueberry
说明组件注册成功。
FactoryBean
Spring提供了FactoryBean
接口,我们可以通过该接口来注册组件,该接口包含两个抽象方法和一个默认方法:
为了演示FactoryBean
的作用,新建一个类Cherry
。
然后创建FactoryBean
的实现类CherryFactoryBean
:
public class CherryFactoryBean implements FactoryBean<Cherry> {
@Override
public Cherry getObject() throws Exception {
return new Cherry();
}
@Override
public Class<?> getObjectType() {
return Cherry.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
最后在配置类中注册即可:
@Bean
public CherryFactoryBean cherryFactoryBean() {
return new CherryFactoryBean();
}
修改启动类,获取cherryFactoryBean
:
Object bean = applicationContext.getBean("cherryFactoryBean");
System.out.println(bean.getClass());
运行启动类,观察控制台,有如下输出:
class top.parak.entity.Cherry
可以看到,虽然获取的是ID为cherryFactoryBean
的组件,但是实际上获取到的是getObject
里返回的对象。
如果要获取cherryFactoryBean
本身,则可以这样做:
Object bean = applicationContext.getBean("&cherryFactoryBean");
System.out.println(bean.getClass());
重新运行,输出如下:
class top.parak.entity.CherryFactoryBean
为什么这里加上
&
前缀就可以获取到相应的工厂类? 查看BeanFactory
源码就可以看到原因:
public interface BeanFactory {
/**
* Used to dereference a {@link FactoryBean} instance and distinguish it from
* beans <i>created</i> by the FactoryBean. For example, if the bean named
* {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject}
* will return the factory, not the instance returned by the factory.
*/
String FACTORY_BEAN_PREFIX = "&";